Skip to content

feat(api): SP-8 · payment rails — DORMANT, re-cut payments-only (founder-gated merge)#79

Open
hizrianraz wants to merge 1 commit into
mainfrom
feat/payment-rails-dormant
Open

feat(api): SP-8 · payment rails — DORMANT, re-cut payments-only (founder-gated merge)#79
hizrianraz wants to merge 1 commit into
mainfrom
feat/payment-rails-dormant

Conversation

@hizrianraz
Copy link
Copy Markdown
Contributor

@hizrianraz hizrianraz commented May 24, 2026

Re-cut: payments-only, DORMANT, conflict-free on current main

This PR was a stale 5-commit sprint omnibus. Re-cut on 2026-05-31 to isolate the one feature that needs deliberate review:

Dropped during re-cut (all already in main):

What remains (this PR): SP-8 payment rails only — built DORMANT behind AINFERA_PAYMENTS_LIVE=0. +1950 / -0 across 15 files (Stripe/CDP/Xendit adapters, charge math, reconciliation, flag gate, payments router, activation runbook).

Verified locally (this exact head):

  • App imports clean — 85 routes
  • 62 payment + contract tests pass, including test_payments_flag_off_inertness (proves the rails are inert with the flag off)
  • Exact-match OpenAPI contract test green

Held for founder merge (not auto-merged): per the settlement/payments founder-gate (Disc #12; MAS PSA legal-review-pre-Seed). The code is dormant and safe to land, but landing payment rails into main is your call. One-click merge when you're ready.

tenant_id=tenant.id,
flattened_msgs=flattened_msgs,
idempotency_key=idempotency_key,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Streaming path ignores vendor passthrough model selection

High Severity

When stream=true, the handler unconditionally delegates to _serve_messages_stream which always calls dispatch_with_brain. Unlike the non-streaming path (which delegates to post_inference with its _is_routed(body.model) check), the streaming path never handles vendor passthrough models (e.g. claude-opus-4-7). A user requesting a specific pinned backend with streaming enabled will have their model choice ignored and get brain-routed to a potentially different model.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c4026f9. Configure here.

max_tokens=body.max_tokens,
temperature=body.temperature,
stream=body.stream,
stream=False,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tools silently dropped in non-streaming messages path

High Severity

InferenceRequest has no tools or tool_choice field, so body.tools from the Anthropic request is never passed to post_inference. Tools are silently dropped in the non-streaming /v1/messages path. The except ToolsNotSupportedError handler (line 327) is dead code since adapters never receive tools through this pipeline.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c4026f9. Configure here.

"rail": rail,
"message": f"webhook to {rail} arrived without its rail-specific signature header",
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Webhook reports wrong error for unknown rail

Low Severity

The signature-header lookup dictionary only contains the three known rails. If an unknown rail value is provided, .get(rail) returns None, and the handler raises a misleading 400 missing_signature_header error instead of reaching select_adapter(rail) which would give the correct unknown_rail error.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c4026f9. Configure here.

…itch

Last build sprint. Three rails (CDP/x402/USDC, Stripe Customer Balance,
Xendit Customer Balance) integrated as code behind a SINGLE master flag
AINFERA_PAYMENTS_LIVE. Default OFF. Every payment path is inert until
the founder flips post-SG-incorporation per docs/payment-activation-runbook.md.

See PR description for full details: master flag + 3 rail adapters +
metering→charge orchestrator (read-only on §16) + webhook router +
reconciliation dry-run + 7-step activation runbook + comprehensive
inertness/margin-math/routing-outcomes-readonly/router tests +
OpenAPI contract updated.

After SP-8, everything Aulë can build is built. Remaining distance
to launch is exclusively founder/legal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hizrianraz hizrianraz force-pushed the feat/payment-rails-dormant branch from c4026f9 to 12b5ea2 Compare May 31, 2026 15:07
@hizrianraz hizrianraz changed the title feat(api): SP-8 · payment rails built DORMANT behind AINFERA_PAYMENTS_LIVE=0 feat(api): SP-8 · payment rails — DORMANT, re-cut payments-only (founder-gated merge) May 31, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 8 potential issues.

There are 11 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issues.

Reviewed by Cursor Bugbot for commit 12b5ea2. Configure here.

cost_actual_usd=cost_actual_usd,
margin_rate=rate,
customer_charged_usd=customer_charged,
debit_record=debit,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ChargeResult margin rate mismatch

Medium Severity

charge_for_inference stores the caller’s raw margin_rate on ChargeResult, but compute_customer_charge clamps out-of-range rates before computing customer_charged_usd. Audit output can show a margin that does not match the amount actually debited.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 12b5ea2. Configure here.

inference_id=inference_id,
amount_usd=customer_charged,
idempotency_key=str(inference_id),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Zero cost still debits

Medium Severity

_read_outcome_cost skips only missing cost_actual_usd, not zero. charge_for_inference then computes a zero customer charge and calls adapter.debit with amount_usd=0, which conflicts with ledger_entries requiring amount_usd > 0.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 12b5ea2. Configure here.

row = result.first()
if row is None or row.cost_actual_usd is None:
return None
return row.agent_id, row.cost_actual_usd
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Outcome lookup ignores duplicates

Medium Severity

_read_outcome_cost uses result.first() on routing_outcomes rows for an inference_id, but that column is not unique. Duplicate rows yield an arbitrary agent and cost instead of failing or picking a defined row.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 12b5ea2. Configure here.

inference_id=inference_id,
amount_usd=customer_charged,
idempotency_key=str(inference_id),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Charge path uncaught rail errors

Medium Severity

charge_for_inference calls select_adapter(rail) and adapter.debit(...) with no handling for ValueError (unknown rail) or NotImplementedError (live stub). Callers get an unhandled exception instead of a skip or structured failure after the DB read and margin math.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 12b5ea2. Configure here.

"message": "live topup wiring lands in the post-SG activation PR",
"rail": adapter.rail,
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Live topup wrong error code

Low Severity

When AINFERA_PAYMENTS_LIVE is on, POST /v1/payments/topup/{rail} still returns detail.code of payments_not_live even though /v1/payments/status reports live: true. Clients and alerts keyed on that code will misread activation state.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 12b5ea2. Configure here.

raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"code": "webhook_signature_invalid", "rail": exc.rail},
) from exc
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Live webhook stub returns 401

Medium Severity

With AINFERA_PAYMENTS_LIVE on, webhook handlers still raise WebhookSignatureError from unwired adapters, and the router maps every such error to 401 webhook_signature_invalid. Processors treat that as a bad signature, not “verification not implemented yet.”

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 12b5ea2. Configure here.

"rail": rail,
"message": f"webhook to {rail} arrived without its rail-specific signature header",
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unknown webhook rail misreported

Low Severity

For POST /v1/payments/webhook/{rail}, an unsupported {rail} path value yields missing_signature_header because header lookup uses .get(rail) before select_adapter. Topup correctly returns unknown_rail for the same mistake.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 12b5ea2. Configure here.

"live reconciliation per-rail lookup lands in the post-activation "
"PR per docs/payment-activation-runbook.md step 7 (post-activation "
"verification — first reconciliation run + 1-cent canary per rail)."
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Live reconcile raises uncaught

Medium Severity

When is_payments_live() is true, reconcile_rail always raises NotImplementedError after summing ledger debits. There is no ReconcileResult return path, so any caller (cron or runbook step) crashes instead of completing reconciliation.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 12b5ea2. Configure here.

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.

1 participant