Skip to content

feat: hosted payment mode — pay for storage via gateway credits - #163

Draft
Nic-dorman wants to merge 18 commits into
masterfrom
feat/hosted-payment-poc
Draft

feat: hosted payment mode — pay for storage via gateway credits#163
Nic-dorman wants to merge 18 commits into
masterfrom
feat/hosted-payment-poc

Conversation

@Nic-dorman

Copy link
Copy Markdown
Member

Summary

Adds payment_mode=hosted: Indelible delegates upload payment to the Autonomi Pay gateway (WithAutonomi/paygate, private) instead of signing locally. Quote batches POST to the gateway's /pay; prepaid tenant credits are debited server-side with verification, idempotency, and async settlement handled there. No wallet is needed on the Indelible host.

What's in the branch (17 commits):

  • payer seam at the evmSigner boundary + HostedPayer — transport-retrying /pay, async poll loop on 202, 402 mapped to a human top-up message with fiat amounts
  • Wallet-less boot (worker/pre-flight/wallet-status gates), payment provenance stamped on uploads (payment_mode + gateway payment key), hosted transaction rows with gateway credit balances
  • Signed-quote relay (include_signed_quotes) so the gateway verifies ML-DSA quotes on its own probe antd before spending
  • Billing surface: /admin/billing in-app Stripe top-ups via admin-scoped relay (tenant key never in the browser), credit history, fiat-primary display across hosted surfaces
  • Fee-aware pre-upload estimates + gross max_gas_fee ceiling (V2-1113) and "≈ N GB remaining at current prices" with methodology tooltip (V2-1114)

Local wallet mode is untouched — every change is hosted-mode-gated.

Draft status — not mergeable yet

  • go.mod carries replace github.com/WithAutonomi/ant-sdk/antd-go => ../ant-sdk-854/antd-go: the signed-quote prepare options ride ant-sdk#242 (open) and need a tagged antd-go release (antd 0.13.0 line) before the replace can drop — CI will fail on this until then.
  • Gateway repo is private while the product is pre-launch.

Testing

  • 18-case e2e suite on the LAN devnet rig (documented as suite runs 8–12 on the Linear tracker): full Prepare→/pay→settle→Finalize round-trips byte-identical, idempotent replay incl. gateway kill/restart, crash-recovery (pre/post-broadcast), refusal + refund cases, live Stripe test-mode card top-up
  • Branch test state: Go 908 pass, vitest 54 pass, npm run build (vue-tsc + vite) clean

Linear

Tracker: V2-916 · principal tickets: V2-923, V2-926, V2-929, V2-930, V2-1086, V2-1097, V2-1098, V2-1100, V2-1113, V2-1114

🤖 Generated with Claude Code

Nic-dorman and others added 18 commits August 25, 2026 08:54
… gateway

payment_mode=hosted (V2-929 PoC): a payer interface at the upload worker's
evmSigner seam, a HostedPayer that POSTs the batch to the gateway's /pay and
maps its 202 unconfirmed onto evm.ErrConfirmationTimeout (so classifyFailure
preserves instead of re-paying), and payment_mode/payment_gateway_url config.
Local-signing default path unchanged. Merkle unsupported in hosted mode
(V2-934). Proven e2e on a local devnet + the dev1 rig.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hosted mode now prepares with include_signed_quotes (antd >= 0.13.0 via
go.mod replace to the branch antd-go) and relays the opaque signed_quotes[]
in /pay so the gateway can verify the batch offline before paying. The payer
seam gains a signedQuotes parameter; localPayer adapts *evm.Signer unchanged
(migrate.EvmPayer untouched). A gateway "rejected" answer is permanent — the
worker abandons rather than retrying.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lance alarm, human insufficient-credits message (V2-930)
…s treat the gateway as payment-ready (V2-929)
A nil Go map marshals to JSON null; antd requires tx_hashes to be an
empty object when prepare reported no payments (chunks already stored).
Caught by the V2-931 suite's dedup case — bites local mode too.
…2-1097)

Admin-scoped server-side relays (checkout / sync / summary) keep the
tenant API key out of the browser; BillingView owns credits, the exact-
credit confirmation before Stripe, and the top-up history; the nav swaps
Wallets→Billing in hosted mode (Wallets redirects); the insufficient-
credits upload error deep-links admins to Billing and tells users to ask
their administrator; Transactions accepts ?type= deep-links.
…, IDR #18)

The counter speaks USD, the ledger stays ANT: shared BigInt money util
(exact rational rate, ≈-labelled nearest-cent display), Billing goes
fiat-primary (credit tile, confirm dialog 'receive $25.00 of storage
credit', history, sync toast) with exact ANT in tooltips; upload cost
rows, hosted transaction rows, and analytics spend tiles convert when a
rate is present; the 402 refusal arrives from the gateway in fiat via
total_usd_cents (ANT only as fallback). wallet-status carries the
gateway rate, cached a minute server-side. Local mode untouched.
A gateway killed with our POST in flight EOFs the connection before the
202+payment_key arrives, failing the upload while the payment itself
survives (debited, broadcast, even paid). The batch is content-addressed
idempotent gateway-side, so resending can never double-pay: retry the
transport (6 attempts, 5s apart — outlasts a restart) and land on the
replay path. Found by V2-931 suite case I on an instamining devnet.
…(V2-1097)

Billing's history now answers 'where did this credit come from' for every
funding path: card rows carry their exact locked USD, invoice/grant rows
convert at the rate (≈, ANT fallback) and carry their note.
… (V2-1098)

The gateway now charges a per-batch network fee with every paid batch, so
the tenant's credits drop by total + fee. The customer's books must say so:

- HostedPayer parses fee_amount/total_debited/fee_usd_cents and stashes
  them per payment_key (PaymentCost, pop-on-read — one payer serves
  concurrent uploads) from both the sync /pay and poll paths
- the worker records the GROSS debit as the upload cost and the hosted
  transaction amount — balance_after math stays exact to the atto — and
  stamps the fee on the upload (migration 017 uploads.gateway_fee_atto)
- the 402 message names both parts, fiat first ("including a $X.YY
  network fee"), ANT fallback, still no crypto leak in fiat mode
- upload detail shows the fee row (fiat via the V2-1100 money util),
  marked included in the actual cost

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gas_fee ceiling

The gateway charges GROSS — batch total + a per-batch network fee (V2-1098)
— but every pre-upload surface still quoted net, understating by one fee per
batch while the actual debit and the gateway's 402 threshold are gross. Now
the estimate and the ceiling speak the same basis as the charge:

- HostedPayer relays fee_per_batch_atto from GET /account (AccountInfo grows
  a third value; absent field from older gateways tolerated as empty) and
  caches it a minute via FeePerBatch — exact big.Int, last-known on gateway
  outage, zero when unknown; never an error
- wallet-status carries gateway_fee_per_batch_atto beside the rate (one
  shared 60s pricing cache, the V2-1100 relay pattern); the admin billing
  summary relays it too
- /uploads/quote adds gateway_fee_per_batch_atto, estimated_batch_count and
  estimated_total_with_fee_atto in hosted mode — one upload settles as
  exactly one gateway batch, so the estimate adds one fee (full dedup at
  prepare time can still drop it: it's an estimate, and says so)
- the max_gas_fee ceiling compares grossUploadCost — net quote + one fee
  when payments exist — so a configured ceiling refuses BEFORE spending
  exactly when the gateway would charge past it; last_quoted_cost and the
  backoff logs record the same gross figure; the hosted payer is now
  ensured before the ceiling (the later ensure block is a no-op)
- web: upload card names the per-upload fee up front (fiat via the money
  util, ANT fallback, exact atto in the tooltip); the detail drawer's
  Estimated row displays gross via grossEstimateAtto (BigInt, no floats);
  Last quoted stays as recorded — it is already gross

Local mode untouched: nil fee keeps the V2-431 net basis everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client half of the storage-capacity estimator (V2-1106 Option 3):

- HostedPayer.AccountDetails relays the gateway's est_cost_per_gb_atto and
  its opaque methodology basis alongside balance/rate/fee; AccountInfo stays
  as a thin wrapper. Absence (older gateway, thin paid history) reads as
  empty fields, never an error.
- wallet-status carries gateway_est_cost_per_gb_atto + _basis via the 60s
  gatewayPricingCache; /admin/billing relays est_cost_per_gb_atto + basis
  from the live AccountDetails call it already makes for the balance.
- Billing view renders '≈ N GB remaining at current prices' under the
  balance: N = balance ÷ cost-per-GB, one decimal, FLOORED, exact BigInt —
  hidden entirely while the gateway has no estimate. The cursor-help tooltip
  is built from the gateway's basis object (median chunk price × chunks/GB +
  network fee, sample size + window, the dedup/market-drift caveat) — no
  methodology numbers hardcoded client-side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
busy_timeout and synchronous are connection-level SQLite pragmas; the
one-shot Exec after Open covered a single pooled connection, leaving the
rest with busy_timeout=0. First bite: V2-1112 coalescing settles several
payments in one tx, so their uploads finalize in the same instant — one
of three concurrent record saves failed SQLITE_BUSY (and the recovery
requeue hit the same lock). Move both pragmas into the DSN, where
modernc.org/sqlite applies them to every connection, alongside the
existing foreign_keys param; journal_mode=WAL stays as the one-shot Exec
(database-persistent, and doubles as the CANTOPEN probe).

Regression tests: the pragma is asserted on several simultaneously held
pool connections, and 8 concurrent write transactions must all commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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