Skip to content

fix(x402): implement background reconciliation for silent orphan payments (#588)#589

Open
Meenbudha wants to merge 1 commit into
GetBindu:mainfrom
Meenbudha:fix/588-x402-reconciliation
Open

fix(x402): implement background reconciliation for silent orphan payments (#588)#589
Meenbudha wants to merge 1 commit into
GetBindu:mainfrom
Meenbudha:fix/588-x402-reconciliation

Conversation

@Meenbudha

@Meenbudha Meenbudha commented Jun 25, 2026

Copy link
Copy Markdown

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: During periods of network congestion or transient timeouts on the Base blockchain, the facilitator's /settle call can time out, leading manifest_worker.py to record the payment status as payment-failed and fail the task. However, the transaction can confirm on-chain shortly after, resulting in a silent debit to the client's wallet with no alert or fallback.
  • Why it matters: Payer wallets are debited USDC on-chain, but the agent fails the request and does not perform the work.
  • What changed: Added a background run_x402_reconciliation_loop worker (in bindu/server/workers/x402_reconciliation.py) that scans recent failed tasks, extracts the payment context from the initial message metadata, and calls the facilitator again (idempotent /settle call) to check if the transaction confirmed. If verified, it updates the task's payment status to payment-orphaned and saves the receipt.
  • What did NOT change (scope boundary): The task's execution state remains failed (we do not re-run the agent or auto-refund since Bindu does not manage an outbound wallet/key custody).

Change Type (select all that apply)

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Security hardening
  • Tests
  • Chore/infra

Scope (select all touched areas)

  • Server / API endpoints
  • Extensions (DID, x402, etc.)
  • Storage backends
  • Scheduler backends
  • Observability / monitoring
  • Authentication / authorization
  • CLI / utilities
  • Tests
  • Documentation
  • CI/CD / infra

Linked Issue/PR

User-Visible / Behavior Changes

List user-visible changes (including defaults/config).
If none, write None.

None

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/credentials handling changed? No
  • New/changed network calls? Yes (Periodic background HTTP request to the configured facilitator's /settle API endpoint)
  • Database schema/migration changes? No
  • Authentication/authorization changes? No
  • If any Yes, explain risk + mitigation: The background loop periodically calls /settle on the facilitator using the exact same client-signed EIP-3009 parameters from the original request metadata, presenting no new authorization exposure.

Verification

Environment

  • OS: Windows 10/11
  • Python version: Python 3.14.0
  • Storage backend: Memory (InMemoryStorage) / PostgreSQL (PostgresStorage)
  • Scheduler backend: Memory (InMemoryScheduler) / Redis (RedisScheduler)

Steps to Test

  1. Run the new integration test targeting the reconciliation loop:
    python -m uv run pytest tests/integration/x402/test_reconciliation.py -v
  2. Run existing E2E integration tests to ensure zero regressions:
    python -m uv run pytest tests/integration/x402/test_e2e_scenarios.py -v

Expected Behavior

A task marked with payment status payment-failed due to a transient timeout should be picked up by the reconciliation worker, verified against the facilitator, and transitioned to payment-orphaned with receipt metadata attached.

Actual Behavior

The task successfully transitions to payment-orphaned, the receipt is stored in task metadata, and the transient error key is cleared.

Evidence (attach at least one)

  • Failing test before + passing after
  • Test output / logs
tests/integration/x402/test_reconciliation.py::test_reconciliation_worker_success PASSED

Human Verification (required)

What you personally verified (not just CI):

  • Verified scenarios: Reconciling failed tasks when the transaction is subsequently confirmed on-chain.
  • Edge cases checked: Ensuring non-failed tasks, tasks without _payment_context metadata, and tasks with other payment statuses are ignored.
  • What you did NOT verify: Live on-chain mainnet settlement (mocked via the facilitator API in integration tests).

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Database migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert the git checkout or comment out the create_task call inside TaskManager.__aenter__.
  • Files/config to restore: bindu/server/task_manager.py
  • Known bad symptoms reviewers should watch for: Background loop throwing unhandled exceptions repeatedly (mitigated by a try/except catcher in the loop body).

Risks and Mitigations

List only real risks for this PR. If none, write None.

  • Risk: Facilitator load/traffic increases from checking failed payments.
    • Mitigation: The background loop only queries the most recent 100 tasks and filters strictly for those in the failed state and payment-failed payment status, creating a small and bounded query set.

Checklist

  • Tests pass (uv run pytest)
  • Pre-commit hooks pass (uv run pre-commit run --all-files)
  • Documentation updated (if needed)
  • Security impact assessed
  • Human verification completed
  • Backward compatibility considered

Summary by CodeRabbit

  • New Features

    • Added automatic background reconciliation for failed payment settlements, helping recover orphaned payments without manual intervention.
    • The system now keeps checking for eligible failed payment tasks and retries settlement in the background.
  • Bug Fixes

    • Improved handling of failed payment states so successful follow-up settlements update task metadata and clear stale error details.
  • Tests

    • Added coverage for the payment reconciliation flow.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new x402 reconciliation worker scans failed tasks, retries settlement, and updates task metadata when settlement succeeds. TaskManager now starts and cancels that background task, and a Hydra auth middleware return statement drops an inline type-ignore comment.

Changes

x402 reconciliation worker

Layer / File(s) Summary
Worker loop entrypoints
bindu/server/workers/x402_reconciliation.py
The new loop sleeps on an interval, calls reconcile_failed_payments, and exits on cancellation while logging loop errors.
Reconciliation settlement path
bindu/server/workers/x402_reconciliation.py
reconcile_failed_payments scans recent tasks, rebuilds payment data from stored context, retries settlement, and updates metadata when settlement succeeds.
TaskManager lifecycle wiring
bindu/server/task_manager.py
TaskManager stores the background task handle, starts the reconciliation loop on enter, and cancels and awaits it on exit.
Reconciliation integration test
tests/integration/x402/test_reconciliation.py
The async integration test seeds a failed task, patches facilitator settlement success, runs the worker, and checks the metadata transition to payment-orphaned.

Hydra middleware cleanup

Layer / File(s) Summary
Hydra middleware return cleanup
bindu/server/applications.py
_create_auth_middleware returns the Hydra middleware without the inline mypy suppression comment.

Sequence Diagram(s)

sequenceDiagram
  participant TaskManager
  participant run_x402_reconciliation_loop
  participant reconcile_failed_payments
  participant Storage
  participant HTTPFacilitatorClient
  TaskManager->>run_x402_reconciliation_loop: create_task(...)
  loop every interval_seconds
    run_x402_reconciliation_loop->>reconcile_failed_payments: invoke
    reconcile_failed_payments->>Storage: list_tasks(...)
    reconcile_failed_payments->>HTTPFacilitatorClient: settle(...)
    HTTPFacilitatorClient-->>reconcile_failed_payments: settlement result
    reconcile_failed_payments->>Storage: update_task(...)
  end
  run_x402_reconciliation_loop-->>TaskManager: CancelledError on shutdown
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • GetBindu/Bindu#563: Updates ManifestWorker settlement-failure metadata that the new reconciliation worker reads and rewrites.
  • GetBindu/Bindu#565: Touches the same x402 payment-status transition path that now feeds reconciliation.
  • GetBindu/Bindu#566: Adds the reconciliation-worker behavior around payment-orphaned recovery for failed settlements.

Poem

I hopped through the x402 night,
Reconciled the payments, made them right.
Failed tasks found their orphaned gleam,
Receipts tucked softly in a dream.
🐇 Thump-thump—ledger stars still bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and accurately summarizes the main x402 reconciliation change.
Description check ✅ Passed The description matches the template and fills the required sections with relevant details.
Linked Issues check ✅ Passed The PR adds the background reconciliation worker, updates task payment status to payment-orphaned, and includes tests, matching #588's core goal.
Out of Scope Changes check ✅ Passed The only extra change is removing a mypy suppression comment, which is minor and still within scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
tests/integration/x402/test_reconciliation.py (2)

43-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Only the success path is covered.

This test exercises settle_response.success is True. The non-confirmed branch (Line 113, status stays payment-failed) and the facilitator-exception branch (Line 120) are uncovered. Adding a "not confirmed" case would guard against regressions that accidentally orphan unconfirmed payments.

Want me to draft the additional test cases for the not-confirmed and exception branches?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/x402/test_reconciliation.py` around lines 43 - 109, The
reconciliation test only covers the confirmed-success path in
reconcile_failed_payments, so add coverage for the other settle_result branches
in x402_reconciliation. Extend test_reconciliation_worker_success or add new
async tests to simulate HTTPFacilitatorClient.settle returning a non-confirmed
response and raising an exception, then assert the task metadata remains
payment-failed and is not changed to payment-orphaned. Use the existing symbols
reconcile_failed_payments and HTTPFacilitatorClient to locate the branch points.

46-46: 📐 Maintainability & Code Quality | 🔵 Trivial

Use the memory_storage fixture instead of instantiating InMemoryStorage inline.

The test file should depend on the shared memory_storage fixture defined in tests/fixtures/storage_fixtures.py, which is already registered in tests/conftest.py.

View diff
- from bindu.server.storage.memory_storage import InMemoryStorage
  ...
- async def test_something():
-     storage = InMemoryStorage()
+ async def test_something(memory_storage: InMemoryStorage):
+     storage = memory_storage

This aligns with the guideline to use fixtures rather than custom setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/x402/test_reconciliation.py` at line 46, The test setup in
test_reconciliation should use the shared memory_storage fixture instead of
creating InMemoryStorage directly inline. Update the test to accept and use
memory_storage from the registered fixture in
tests/fixtures/storage_fixtures.py, so the reconciliation test relies on the
shared test infrastructure rather than custom storage construction.

Source: Coding guidelines

bindu/server/workers/x402_reconciliation.py (2)

35-47: 🚀 Performance & Scalability | 🔵 Trivial

Repeatedly-failing tasks are re-checked every tick with no backoff or attempt cap.

Tasks that stay payment-failed (genuine failures, not transient timeouts) match the filter on every 30s scan of the most recent 100 tasks, each time validating models, constructing a fresh HTTPFacilitatorClient, and re-calling settle. Consider a max-attempt counter or last-checked timestamp in metadata, and reusing a single facilitator client, to bound facilitator load.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindu/server/workers/x402_reconciliation.py` around lines 35 - 47, The
reconciliation loop in reconcile_failed_payments keeps reprocessing the same
failed tasks on every scan, which can repeatedly call validate models, create a
new HTTPFacilitatorClient, and retry settle without any bound. Add a retry guard
using task metadata such as a max-attempt counter or last-checked timestamp so
terminal failures stop being rechecked every tick, and reuse a single
facilitator client within the reconciliation flow instead of constructing one
per task.

100-104: 📐 Maintainability & Code Quality | 🔵 Trivial

Define a canonical status_orphaned constant and use it.

While app_settings.x402.status_failed exists, the specific value "payment-orphaned" is currently hardcoded. Add a corresponding status_orphaned constant to bindu/settings.py to ensure consistency:

# bindu/settings.py
class X402Settings(BaseSettings):
    # ... existing fields
    status_failed: str = "payment-failed"
    status_orphaned: str = "payment-orphaned"  # Add this line

Then update the code to reference the new constant:

# bindu/server/workers/x402_reconciliation.py
updated_metadata = {
    **metadata,
    app_settings.x402.meta_status_key: app_settings.x402.status_orphaned,
    app_settings.x402.meta_receipts_key: [settle_response.model_dump()],
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindu/server/workers/x402_reconciliation.py` around lines 100 - 104, Add a
canonical X402 orphaned status constant in X402Settings inside bindu/settings.py
and use it from x402_reconciliation instead of hardcoding "payment-orphaned".
Update app_settings.x402 to expose status_orphaned alongside status_failed, then
replace the literal in updated_metadata within x402_reconciliation with
app_settings.x402.status_orphaned so the status value stays consistent across
the codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bindu/server/task_manager.py`:
- Around line 192-201: The shutdown path in TaskManager’s reconciliation cleanup
is fine, but the warning log in the exception handler is using printf-style
formatting with loguru, so the exception detail won’t be rendered. Update the
logger.warning call in the reconciliation task cancel/await block to use
loguru’s "{}" placeholder formatting, preserving the existing message and
exception variable so the error text is actually interpolated.

In `@bindu/server/workers/x402_reconciliation.py`:
- Around line 31-32: The logging in x402_reconciliation uses printf-style
placeholders, which Loguru ignores, so dynamic error details are dropped. Update
the logger.exception call in x402_reconciliation.py to use Loguru-style {}
formatting, and review any other log statements in the same file to ensure they
use {} instead of %s so messages and exception context are preserved.

---

Nitpick comments:
In `@bindu/server/workers/x402_reconciliation.py`:
- Around line 35-47: The reconciliation loop in reconcile_failed_payments keeps
reprocessing the same failed tasks on every scan, which can repeatedly call
validate models, create a new HTTPFacilitatorClient, and retry settle without
any bound. Add a retry guard using task metadata such as a max-attempt counter
or last-checked timestamp so terminal failures stop being rechecked every tick,
and reuse a single facilitator client within the reconciliation flow instead of
constructing one per task.
- Around line 100-104: Add a canonical X402 orphaned status constant in
X402Settings inside bindu/settings.py and use it from x402_reconciliation
instead of hardcoding "payment-orphaned". Update app_settings.x402 to expose
status_orphaned alongside status_failed, then replace the literal in
updated_metadata within x402_reconciliation with
app_settings.x402.status_orphaned so the status value stays consistent across
the codebase.

In `@tests/integration/x402/test_reconciliation.py`:
- Around line 43-109: The reconciliation test only covers the confirmed-success
path in reconcile_failed_payments, so add coverage for the other settle_result
branches in x402_reconciliation. Extend test_reconciliation_worker_success or
add new async tests to simulate HTTPFacilitatorClient.settle returning a
non-confirmed response and raising an exception, then assert the task metadata
remains payment-failed and is not changed to payment-orphaned. Use the existing
symbols reconcile_failed_payments and HTTPFacilitatorClient to locate the branch
points.
- Line 46: The test setup in test_reconciliation should use the shared
memory_storage fixture instead of creating InMemoryStorage directly inline.
Update the test to accept and use memory_storage from the registered fixture in
tests/fixtures/storage_fixtures.py, so the reconciliation test relies on the
shared test infrastructure rather than custom storage construction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: abfbffac-6a69-4280-85d9-3099d798a3e9

📥 Commits

Reviewing files that changed from the base of the PR and between e9e82b9 and 9aed4b5.

📒 Files selected for processing (4)
  • bindu/server/applications.py
  • bindu/server/task_manager.py
  • bindu/server/workers/x402_reconciliation.py
  • tests/integration/x402/test_reconciliation.py

Comment thread bindu/server/task_manager.py
Comment thread bindu/server/workers/x402_reconciliation.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: manifest_worker fails closed on transient facilitator settle timeouts instead of flagging payment-orphaned

1 participant