Reference
Changelog
Phala Pay's releases. The service, the JS SDK packages @phala/pay (sdk/js),
@phala/pay-react (sdk/js-react), and @phala/ (sdk/js-server), and the Python SDK
phala-pay (sdk/python) share one version and are released together (CONTRIBUTING.
Unreleased
JS SDK (@phala/pay , @phala/pay-react , @phala/pay-server )
Fixed
- A token or network logo whose disc is the color of its background no longer loses its edge
(PHA's black disc on a dark theme, Ethereum's white disc on a light one):
NetworkIconandAssetIcondraw a faint 1px ring inside the logo's edge, the new optional--pp-(default: the surrounding text color at 12% opacity), andcolor- icon- ring GrayTextin forced-colors mode. It is in the static stylesheet, over the unchanged logo.
0.11.3 - 2026-10-23
Changed (operators)
- Update the service and local dstack simulator's Rust builder images to 1.99.0 and the
reference product's uv/Python image to uv 0.13.5 on Python 3.14. Rebuilding the service and
reference-
product images changes their image digests and rendered compose hashes. - Refresh grouped Rust, Python, JavaScript SDK, website, and API-reference dependencies, plus CI and deployment Actions.
Fixed (operators)
- Rollback release resolution reports missing or invalid release tags, version errors, and
policy failures explicitly. GitHub API and attestation-
download reads retry transient transport and server errors within the existing deadlines. Each downloaded attestation bundle is verified once; a verification rejection still fails immediately.
Security
- Update the website's DOMPurify to 3.4.17 and ip-address to 10.7.4, and Python's
opentelemetry-
api to 1.45.1, for their published security advisories.
JS SDK (@phala/pay , @phala/pay-react , @phala/pay-server )
Changed
- Token and network icons are CoinMarketCap's logos instead of
@web3icons/core's: USDC, USDT, PHA, and ETH, and Ethereum and Base as CoinMarketCap shows them. Each is a 64px PNG embedded as adata:URI in the same inline SVG, so a page requests nothing for it, but its Content Security Policy must allowimg-src data:, as the SDK already asked for browser wallet icons.NetworkIconandAssetIconkeep their props and sizes and draw every logo round, networks too;assetIcon()andnetworkIcon()markup is the image as CoinMarketCap publishes it, round it withborder-radius: 50%. The logos are trademarks of their owners (THIRD_);PARTY_ NOTICES @phala/, which has no icons, no longer ships a notice.pay- server
0.11.2 - 2026-10-22
Security
- Update the website's Mermaid diagram tooling to KaTeX 0.18.10 for CVE-2026-103923 and remove its Trivy exception.
JS SDK (@phala/pay , @phala/pay-react , @phala/pay-server )
Added
@phala/pay-react's<PaymentElement>takeschildren: your content inside its form, under the token and network, while the payer chooses. WithhideSubmit, your summary and yourtype="submit"button go there, and the deposit-address line comes after your button.
Changed
- Token icons are one set: each token is its
@web3icons/corebackgroundtile, drawn byAssetIconas a same-size coin with its glyph inset alike, instead of mixed shapes (a circle, a bare diamond, and a PHA glyph edge to edge on a dark disc). Network icons stay theirbrandedlogos. The vendored icons are from@web3icons/.core@4. 0. 58 assetIcon()markup is the square tile; round it withborder-radius: 50%. <PaymentElement>'s deposit-address offer is one quiet line at the end of the form: "Paying from an exchange? Use a deposit address". The per-transfer limits under it (0.11.1) moved to the deposit address itself, which states them. The element's parts are evenly spaced.
0.11.1 - 2026-10-22
JS SDK (@phala/pay , @phala/pay-react , @phala/pay-server )
Added
@phala/pay-react's<PaymentElement errorMessage>:(cause) => string | null | undefined, the alert to show whencreateQuoteorcreaterejects, such as your backend's reason, instead of the element's own "Could not start the payment. Try again." beside yours.Deposit Address null,undefined, or an empty string keeps the element's text. A retry reuses the same idempotency key, as before.
Fixed
@phala/pay-react's<PaymentElement>no longer says a deposit address takes "any amount". Under "Use a deposit address" it states the chosen option's per-transfer limits when the option carries them, as aGET /v1/configasset does: "Send at least $5.00 and up to 10,100 USDC per transfer on Base from an exchange or another wallet." Without them it names the token and network and says the limits are shown with the address, which<DepositAddress>states.
0.11.0 - 2026-10-21
Changed (operators)
- Breaking: make staging and the Phala Cloud template USDC-only on Base Sepolia, matching
Base's production catalog; Sepolia keeps PHA, USDC and USDT. Remove Base Sepolia PHA/USDT from the
reference demo and its test-token faucets. Before upgrading staging, keep the retiring routes'
quotes paused and drain every open lock and in-flight deposit, including credited deposits
awaiting sweeps (retirement steps). Require
zero open exposure and zero detected, confirmed, credited and credited-
undelivered counts for both retiring routes. Then upgrade staging topup and the reference product together and verify /v1/config, the demo choices and faucets. Retain the verified N-1 kit, images and config for rollback; route state and pauses survive. After removal, PHA/USDT sent to old Base Sepolia addresses are unsupported, and their later sweeps and reversals are no longer recorded.
Security
- Update the Rust and Python TLS trust dependencies and rebuild smokescreen, gosu, and WAL-G
with Go 1.27.3 to reject distrusted emSign certificate paths. Update aiohttp and the website's
Vite and Hono dependencies for their published security fixes. The service image (which
contains smokescreen) and
postgres-walgimage are rebuilt, changing their image digests and the rendered compose hashes.
JS SDK (@phala/pay , @phala/pay-react , @phala/pay-server )
Added
@phala/pay-react's<PaymentElement>: a complete payment that is created only when the payer submits, like Stripe's Payment Element with a deferred intent. It offers your tokens and networks; on submit it calls yourcreate, which creates the quote on your backend with that key as itsQuote(option, { idempotencyKey }) Idempotency-Keyand resolves withpay., then shows that quote's checkout, with a way back to the options before a payment is sent or once the quote ended. A failedcheckout Params(quote) createQuoteshows "Could not start the payment. Try again." (and callsonError); a submit while one is pending is ignored. The key is a new random UUID for each quote the payer asks for and is repeated only to retry, after a failure, with the same option,amount, andcurrency. Withcreate, it also offers the customer's deposit address.Deposit Address hideSubmitwithidlets your own<button type="submit" form={id}>submit it.onChangereports its step and the checkout's status;onSuccess,onExpire,onWalletError,walletClient, andappearancework as<Checkout>'s.@phala/pay-react's<Payment: the token, then the network, as two select-only fields side by side (stacked under a 480px-wide container), as exchanges and wallets ask for them. Each is a WAI-ARIA combobox with a listbox of icons and names, by pointer or keyboard; the network's listbox offers only the networks that take the chosen token, and changing the token keeps the network where it can. The open listbox is in the top layer (the Popover API, where the browser has it), soOption Picker> overflow: hiddencards and modals do not clip it, and it opens upward when there is more room above; it is placed through the CSSOM, whichstyle-src 'self'allows. A press elsewhere closes it, and a field's label focuses it. A test network shows a muted "Testnet" after its name. Both fields show however few options there are. It callsonChangewith the very object ofoptionsthe payer chose;renderDetailadds one line under the fields about the chosen option, such as its price.@phala/pay'spaymentflattensOptions(source) GET /v1/config'sassets, or a deposit address'snetworkswith their tokens, into options in order and without duplicates;same,Payment Option payment(Option Label USDC · Ethereum),assetName(USD Coin),assetSymbol,isTestnet, andaddressUrl(an explorer link, astransactionUrl).
Changed
- Breaking:
<DepositAddress>asks for the token and network with<Payment's "Token" and "Network" fields, always, even with one option, instead of separate "Network" and "Token" radio groups shown only with more than one choice. TheOption Picker> .pp-choices*classes are replaced by.pp-options*and.pp-select*; a stylesheet that targeted them should use the--pp-*properties instead.--pp-now colors only the selected tab; listboxes usecolor- selected --pp-.color- surface <Checkout>and<DepositAddress>acceptundefinedfor their optional props, so a host withexactcan pass its own optional values through.Optional Property Types
0.10.10 - 2026-10-18
Changed (operators)
- Add attested
hints_per_dayandquote_configuration keys, integers from 1 to 1,000,000 with defaults 80/60. Production sets H=200/Q=150 and raises operational deposits to D=115/day and factory receipt verifications to 150/day; Safe proof sets stay 10/day, refunds R=2/N=1 and historical address capacity 1,000/chain. Staging stays unchanged, including factory=20/day. The shared Ankr budget is 20,929 calls/day (16.28% headroom); production BlockPI is 372,030.5 RU/day, rounded up to 372,031 (70.24% headroom).snapshots_ per_ day - Expose
topup_beside persisted usage and compare them in the RPC exhaustion alerts. Rollback to older releases requires their verified configuration, kit and alert rules: their strict parser rejects the new keys. Keep the existing PROXY rollback requirements; retaining DB compatibility does not make old binaries honor the new caps.daily_ budget_ limit{name}
Fixed (operators)
- Restore attestation accepts exactly the service's inherited port policy: PROXY on for 443,
restrict_.mode: false instances addcopies that field from the source app-compose; requiring PROXY off rejected a booted restore-check instance. Port 443 is unbound in restore-check, which publishes plain HTTP only on 8081, so the inherited setting is inert. Service, product, and template policies are unchanged; production needs no upgrade. - Restore checks use the app's allowed
AWS_andACCESS_ KEY_ ID AWS_names with a fresh read-only token in an explicit restore env file.SECRET_ ACCESS_ KEY instances addretains the source allowlist, so dstack dropped the formerRESTORE_AWS_*values and PostgreSQL could not restore. Preflight requires both restore credentials even with--unsealed; the local drill selects only its read-only token. The service compose is unchanged; production needs no upgrade. - The production restore drill waits up to ten minutes for the new instance's attested instance id before verification, with bounded requests and live-isolation checks during the wait.
0.10.9 - 2026-10-16
Breaking (operators)
- Service client-IP passthrough ships in v0.10.9. Rolling back to an earlier release requires
that release's compose-update path from a v0.10.8 or newer kit, which explicitly sets
pp:falsetogether with PROXY-off ingress settings. Never roll back directly to v0.10.7 or older.
Changed (operators)
- Enable gateway PROXY on service port 443 and ingress ACCEPT/SEND atomically with topup's private PROXY v2 listener on 8082. Product, template, and restore-check keep PROXY off. Public health, certificate evidence, and tls-alpn-01 use the same domain; internal HTTP checks keep port 8080. Document staging verification without adding an IP diagnostic endpoint or topup IP logging.
Fixed (operators)
- Make compose-update reruns succeed when the provisioned compose hash already matches the CVM's current hash, skipping the redundant commit while keeping the exact-hash wait and attestation checks. Registered hashes that differ from the current hash still require a commit.
0.10.8 - 2026-10-16
Added (operators)
- Optional private
topup run --proxy-listener requiring PROXY v2, with concurrent five-second header reads, bounded pending connections, the existing router and deadlines, and graceful shutdown. Carries the source in connection information without logging it to tracing or Sentry or introducing per-IP limits.protocol- bind ADDR
Changed (operators)
-
Pin dstack-ingress 2.8 for topup and the reference product, with PROXY off and backend ports unchanged. Deploy upgrades and provisioning follow-ups use the compose-file API to set and verify an explicit attested
pp:falsepolicy while preserving the sealed environment. -
Retry temporarily unavailable ingress evidence (HTTP 404) within the existing request and Deploy deadlines. Ingress 2.8 republishes evidence before serving on startup and withdraws it while regeneration fails, with evidence retries independent of HAProxy reloads.
-
Phase 2 will enable gateway PROXY and ingress ACCEPT/SEND together in a separate release. Once phase 2 ships, no rollback past this release; rollback must first restore PROXY-off settings atomically. HAProxy's private access logs may contain client IPs, an accepted exception with
public_; topup's logging privacy rules remain unchanged.logs= false -
Approve the non-dev
dstack-0.6.1OS image and verified hash in all five environments; use verifier 0.6.1 with its authenticated manifest and all nine measurement files. When the measurement archive is unavailable, verification downloads the checksum-pinned ~617 MB mkosi release. CI's simulator uses the 0.6.1 guest source. Keepdstack-sdk =0.1.3and v0GetKey: an in-place OS upgrade preserves app identity, webhook keys, backup keys, database passwords and the encrypted data disk. Take and verify a fresh backup first. OS rollback is a PATCH back todstack-0.5.12with the previous kit; keep the app-compose manifest version and follow the existing schema rollback policy.
0.10.7 - 2026-10-14
Changed (operators)
- Approve the non-dev
dstack-0.5.12OS image and its verified hash in all five deployment environments after Phala Cloud retired 0.5.9. Keepdstack-sdk =0.1.3and existing app-derived keys. Upgrade preflight checks the existing CVM's OS-image allowlist; Deploy changes the image in place when needed, waits for the old compose to recover, then takes a fresh maintenance lease before deploying compose. Provision uses 0.5.12 directly; CI's simulator uses its guest source. OS measurement verification can use the checksum-pinned official meta-dstack release when the measurement download is unavailable, with a fresh authenticated verifier cache.
Fixed (operators)
- Keep upgrade availability probes running through both OS-image and compose reboots until the deployment step ends, within its 60-minute budget. Record every outage window and the summed duration; interrupted sampling or an open outage reports only a lower bound.
- Require observing the OS-image operation start before accepting its settled running state, preventing an early image-name update from racing the compose deployment.
0.10.6 - 2026-10-14
Security
- Updated
rustlsto 0.23.46, fixing RUSTSEC-2026- 0346 through RUSTSEC- 2026- 0349: TLS session secret exposure in Debugoutput, application data sent after ECH rejection, excessive Brotli certificate decompression allocation, and unauthenticated key-exchange downgrade prediction.
0.10.5 - 2026-10-14
Added (API)
- Deposit-address assets in merchant and client views include
min_amount(minimum credit in USD cents),min_, anddeposit_ atomic max_(inclusive token bounds as atomic-unit strings), resolved from the account’s payment settings and route at view time, using screening’s resolver. Admin restore responses omit all three fields because reconstruction ignores held payment settings.deposit_ atomic
Changed (operators)
- Production reads with Ankr and verifies with BlockPI free on Ethereum and Base, using separate
sealed
TOPUP_/RPC_ BLOCKPI_ ETHEREUM_ KEY TOPUP_mappings and 1,000-block log windows under the measured 1,024-block cap. Production removesRPC_ BLOCKPI_ BASE_ KEY TOPUP_; staging stays on Ankr + Infura. The launch gate is production's reviewed allocation and preflight before provisioning, replacing the staging dRPC prerequisite. Per-account budgets and quota alerts cover shared Ankr, staging-only Infura and production-only BlockPI; configured D=46/20 and every other cap stay unchanged.RPC_ INFURA_ KEY - Deploy accepts optional Environment variable
PHALA_NODE_ID(empty means automatic placement), passes it only when provisioning, and records requested and actual node placement in the run summary. Upgrades keep the CVM's node.
Fixed (operators)
- Deploy logs the Phala Cloud CLI's JSON output even when it fails and preserves its exit status, so failed provisioning reports its error and request id.
JS SDK (@phala/pay , @phala/pay-react , @phala/pay-server )
Fixed
<DepositAddress>replaces “Any amount is credited” with “Eligible transfers are credited” and shows the selected token/network’s USD credit minimum and token maximum, plus the atomic minimum when nonzero. Token amounts use English thousands grouping without precision loss; malformed merchant-supplied limits omit the range instead of crashing. Public-view reads refresh the limits; older servers without bounds use neutral copy without a range. Discovery plus confirmation credit-time estimates are unchanged.
Added
- Deposit-address types carry the credit minimum and per-transfer token bounds, including the public-view parser and regenerated server SDK types and schemas.
Python SDK (phala-pay )
Added
- Regenerated
DepositincludesAddress Asset min_amount(minimum credit in USD cents) and the per-transfer minimum and maximum atomic amounts.
0.10.4 - 2026-10-13
Changed (operators)
ForwarderFactory0x45466D37587E6E46DC35eB96b74ba3D3b1E5b747and its forwarder implementation are deployed on Ethereum mainnet and Base mainnet and verified on two providers per chain (deploy/CONTRACTS. md, "Mainnet"). The daily contract verification no longer treats chains 1and8453as pending, so an empty or mismatched deployment on either chain now fails it.
Fixed (operators)
- Coverage uses separate Transfer-
recipient and factory-address log queries so Ankr accepts the full 1,000-address cap; topup rpc checkexercises those same queries. Receipt-log self-tests tolerate optionalblockTimestampmetadata, and dual contract/preflight checks compare both headers at the lower finalized height, with contract failures reporting their result or error. The production launch allocation uses Ankr read + BlockPI verify while staging stays on Ankr + Infura; caps and runtime checkpoint advancement remain unchanged.
0.10.3 - 2026-10-12
Fixed (operators)
- Pump persistence takes the chain advisory lock before deposit rows, matching coverage and finality and preventing a pump/coverage deadlock. Frozen chains still permit waits and retries.
- Coverage reads unmarked deposits only for the addresses and stored block ranges covered by that round. Out-of-range deposits, including unresolved and slow confirmations, keep their own shared claim and backoff instead of consuming unbudgeted hourly receipt reads.
0.10.2 - 2026-10-12
Fixed (operators)
- A confirmation claim that declines a deposit now releases its row lock before returning instead
of holding it until the pool's background return task sends the queued
ROLLBACK, which could make the finality watcher skip that deposit for a tick. Refund verification, finality evidence and reversal, scanner snapshot retries, hints, webhook disabling, treasury restoration, and metadata updates also release their locks before returning.
0.10.1 - 2026-10-12
Fixed (operators)
- Repair the daily contract verification workflow for the
rpcread/verify configuration: resolve both environment configs in independent matrix jobs, map public RPCs through the contracts network registry, reuse the full deployment checker and daily Sepolia Safe check, and compare route addresses with the deployment report. Reviewed pending chains1 8453permit only two exactly empty code results with warnings and summaries; remove each in its deployment-evidence PR. Service RPC keys remain outside GitHub. - Replace the production v0.10.0 launch runbook with the ordered owner procedure and sign-off
checklist. Document the accepted shared-account pilot budget, staging SDN prerequisite, manual
monitoring and uptime risk, optional staging separation, website CSP decision, and first-launch
recovery. Run from a private work directory, shred local key copies, bind every dispatch to
the approved main commit, and verify drill deletion by its saved VM id. Track staging's move
with an owner and batch production-
attested comment edits with the next upgrade. Align the RPC, sanctions, alert and integration references with v0.10.0 behavior.
Python SDK (phala-pay )
Documentation
- Describe the required
Phalaconstructor and deployment trust pins in both modes, replacing the removed individual constructor pin arguments and test-mode fallback.Pay(api_ key, pins=...)
JS SDK (@phala/pay , @phala/pay-react , @phala/pay-server )
Added
@phala/pay-react's--pp-theme token colors the selected tab's underline and the checked network or token choice's border. It defaults tocolor- selected --pp-, so existing themes look the same. A host whose primary color is a light fill (a lime pay button on a white card) sets it to a color with 3:1 or more contrast against the background, as WCAG 1.4.11 asks, instead of stylingcolor- primary .pp-choices__input:checkedor.pp-tabs__tab[aria-selected]. Forced-colors mode still draws the selection inHighlight.
Fixed
<Checkout>'s browser wallet button's accessible name now matches its visible text (Pay with crypto <wallet>, for example "Pay with crypto MetaMask") instead ofPay with crypto (<wallet>)(WCAG 2.5.3, axelabel-). Integrations and tests that select the button by its accessible name must update the name.content- name- mismatch <Checkout>'s browser wallet button no longer breaks its label or the wallet's name across lines on a narrow screen ("Pay with / crypto Test / Wallet" at 320px): each stays whole, and the wallet's name goes under the label where both do not fit.<DepositAddress>promised a payer the hinted wallet-payment time, for example "credited at the market rate on arrival, usually in about 30 seconds on Sepolia and about 7 seconds on Base Sepolia".<DepositAddress>submits no transaction-hash hint (the payer's own wallet or exchange gives it no hash), so since v0.10.0 such a transfer waits for the 5-minute discovery scan before confirmation. Each network's time is now that scan plus the public view'stypical_: "credited at the market rate, usually within about 6 minutes on Sepolia and about 5 minutes on Base Sepolia".credit_ seconds <Checkout>is unchanged.
0.10.0 - 2026-10-10
Changed (operators)
- Confirmation uses bounded head-only probes: Depth six probes at the fixed estimated-depth
anchor plus 4/12/28/60/124 seconds, Safe three and Finalized seven at 384-second intervals.
Height-only exhaustion enters slow lane L and rechecks after 60 seconds, ten minutes, then
hourly; it confirms when both heads qualify without waiting for checkpoint. DB gauges alert
on ten-minute L occupancy and current/
rolling- entry totals above one per environment. Missing/ conflicting evidence stays in S under one shared atomic read lease and backoff; terminal evidence flows directly to valuation. Re-inclusion can change position but cannot change transfer identity. Price retries reuse the exact new-path proof linked atomically to the current final marker; legacy markers and proofs predating S require fresh watcher evidence. Finalized price failures retain the ten-method per-endpoint ceiling. Expand-only counters/history survive rollback, but N-1 keeps its old RPC allocation. Production's operational deposit allocation is 46/day (staging 20) to retain 10.04% shared Infura budget headroom.
Breaking (operators)
-
Replace the deprecated Chainalysis oracle with verified OFAC SDN snapshots and audited manual supplements. Allow outbound HTTPS to the SLS and its published S3 host. The service verifies the publication hourly; without a fresh snapshot, negative answers hold (default limit 24h).
chain.remains parsed for N-1 rollback and is removed in N+1. Before a production rollback, pause all screening-sanctions_ oracle dependent fund processing, regardless of manual entries, until N is restored and verified screening is healthy. A passing database/binary rollback drill does not authorize settlement with N-1's stale oracle. -
The pilot permanently caps issued addresses at 1,000 per chain, counting all historical addresses. Quotes and deposit addresses return non-retryable
422 address_at the cap. Operators must run the pre-upgrade count check; alerts warn at 70% and 90%. Capacity growth requires a reviewed paid-provider or token-wide scanning/indexer upgrade.capacity_ reached -
Replace RPC company, budget and group registries with one strict
rpcread/verify pair per route or price chain. Seal the complete secret set withTOPUP_andRPC_ ANKR_ KEY TOPUP_. Deploy preflight checks the actual compose environment path.RPC_ INFURA_ KEY -
Quote cancellation requests keep the quote open and its exposure reserved until its address catches up to dual finalized coverage past
expires_at. Quote objects addcancel_; completion emitsrequested_ at quote.canceled. Expiry uses the same evidence gate. -
Fresh quote price snapshots are capped at 60 per price chain/
environment/ UTC day. Exhaustion returns retryable 503 price_; the twelve-second reuse limit is unchanged. Production and staging share provider free quotas: their operational deposit allocations are 46/day and 20/day respectively. Keep hourly dual custody on every routed chain/token pair; the required positiveunavailable max_deployment setting enforces production's limit of two attached-pending refunds and staging's limit of one per environment.attached_ pending_ refunds -
Remove RPC recovery/resume commands, member pools, review sweeps, single-source backstops and custom head-poll flags. The expand-only migration preserves rollback to the prior stable release, whose RPC frozen/
anchor/ recovery state must be resolved before starting this version.
Added
-
Admin-signed manual sanctions add/remove endpoints with transactional audit, snapshot provenance in screening evidence and daily reports, and sanctions refresh/
freshness metrics, alerts and an operator runbook. Activating a snapshot re-screens current and pending treasuries and pending refund destinations across all EVM chains. -
API-key authentication uses a database slot gate sized to half the pool (at least one slot). A checksum-valid Bearer key waiting more than 250 ms returns
503 unavailablewithRetry-After: 1; the request has not executed. Malformed or checksum-invalid keys still return401without taking a slot. Payer reads, health checks, and admin authentication use their existing admission paths. -
Object-scoped transaction-hash hint endpoints for quotes and deposit addresses acknowledge admitted requests with
202 received, use client-secret or merchant write authentication, and only record dual-verified successful routed transfers at confirmation. Hard call, time, concurrency and UTC daily task limits fall back to scanning; hints never establish negative coverage. Complete evidence is fetched independently after both endpoints reach confirmation depth; receipt lag and confirmation-time reorgs stay within the whole-task budget. Endpoint readiness loss parks tasks, and cancellation or panic releases running tasks. Hint counters expose budget claims, recorded deposits, exhaustion, deferral and parking. Object credentials gate rate limits; hint endpoints omit peer-IP limits because TCP ingress shares one peer across all clients. -
Dual finalized coverage and per-address backfill markers, independently verified deposit evidence, and durable agreed checkpoints. Contract code mismatch freezes only its chain; audited lifts require a fresh passing dual-source check.
Changed
-
Confirm and finality-watch checks of absent transfers share durable once-only unresolved entry timestamps and the one-minute/ten-minute/hourly schedule. Due unresolved deposits at the persisted finality checkpoint are checked only by the watcher; agreed positive transfer evidence returns provisional correction and valuation to confirm, retaining unresolved entry history. Coverage freezes immediately when dual-agreed boundary hashes contradict persisted checkpoint or coverage evidence. Refund concurrency is a required positive deployment setting (production 2, staging 1); the one-
new- attachment rolling-24-hour limit and reservation safety are unchanged. -
Unresolved deposit finality checks back off from every minute for the first ten minutes unresolved, to every ten minutes until six hours, then hourly; normal newly due finalization and the one-hour pending-
after- reorg alert are unchanged. Only one service-known replacement candidate may be read on both endpoints; multiple candidates alert and keep the deposit unresolved. DB-derived per-chain gauges track current unresolved stock immediately and distinct first unresolved entries over the rolling 24 hours, retaining resolved entries in that window. Either environment total above one alerts; operators must pause new quotes until both counts return to at most one. The first unresolved timestamp survives rechecks and rollback. -
Payment discovery runs every five minutes, dual finalized checkpoints are independently verified and published every ten minutes, and complete dual log coverage runs hourly. Observation-
chain contract recovery remains every minute and admitted hint processing keeps its seconds-scale fast path. Without hints, discovery adds up to five minutes (mean 2.5), plus confirmation and processing. Newly due deposit finality/ reversal and checkpoint- conflict detection add up to ten minutes plus processing; log-only conflicts still wait for coverage. Expiry, cancellation completion and reservation release still require dual coverage: allow chain finality plus ten minutes for the checkpoint, one hour for coverage and five seconds for expiry. Coverage lag now warns after two hours; Sentry monitors track all three independent cadences. Every sixth coverage round scans up to 19,200 blocks (every six hours); normal rounds retain 3,000 blocks. Healthy continuous recovery can clear a 24- hour- equivalent backlog within twelve hours, subject to the recovery-day deposit/factory operational caps. Failures, restarts and address backfill extend these bounds. Custody remains hourly and coverage-pinned, so a newly finalized discrepancy can take about 130 minutes plus RPC time to freeze the chain. Chain-time quote eligibility is unchanged; later processing can change fresh valuation and sanctions results. -
Breaking: Each environment enforces its configured attached-pending refund cap (production two, staging one) and admits one new refund transaction attachment per rolling 24 hours across all merchants and modes.
mark_paidreturns non-retryable422 refund_when either limit would be exceeded; contact the operator before sending or attaching another payout. Refusals keep the pending reservation, idempotent repeats consume no quota, and existing attachments continue dual-source verification. Admitted transaction hint tasks are capped at 80 per environment/UTC day; fresh quote snapshots remain capped at 60 per price chain/attachment_ limit_ exceeded environment/ UTC day, with alerts at both caps. -
Breaking: Refund transactions never seen by either endpoint after 24 hours remain
pendingand keep their reservation, withTopupalerting the operator. They no longer becomeRefund Progress Age failedwithtransaction_or emitnot_ found refund.failed; merchants cannot refund the reserved amount again or cancel aftermark_paid. Attached refunds are checked every 60 s for the first 30 min, every 10 min until 24 h, then hourly until finalized verification resolves them. Historical failure values remain readable. -
Breaking: Global API overload on transaction-hint endpoints returns the standard retryable
503 unavailableinstead of202 received, before reading the request body. Admitted hints retain their quiet acknowledgement behavior. -
Removed the per-source pre-
authentication budget: Phala's TCP ingress exposes only the shared gateway's WireGuard address, so one client could exhaust it and cause every merchant to receive 429. There is no per-client-IP limiting; see API admission limits for the protection model. -
Quote pricing failures now return retryable
503 price_instead ofunavailable unavailable. Routed chains that are not ready return503 chain_.unavailable -
Compliance-
relevant behaviour change: Screening uses the latest verified local snapshot at decision time. A hit in the active OFAC snapshot or manual list rejects even when stale or the other list read fails. A negative answer clears only with a fresh snapshot and successful list reads; otherwisesanctions_holds and retries. For credit delivered before a restore, a hit preserves the credit and blocks its sweep; an uncertain answer holds without changing delivered value.inconclusive -
Breaking:
topup restore-check --expected-is nowheartbeat- at --failure-at, and the restore report'sexpected_JSON field is nowheartbeat_ at failure_at. Both refer to the externally recorded failure instant. -
Request deadline errors now say "the request did not complete within its deadline; retry" instead of blaming the database, with
503 unavailableandRetry-After: 2. -
Price adapters are shared across routes and coalesce concurrent fetches. Quotes may reuse on-chain observations for up to 12 s, with source freshness checked on every use; crediting (confirm) always fetches fresh and only coalesces concurrent fetches. CEX prices and sequencer uptime only coalesce. TWAP sampling fetches each shared pair once per interval.
-
Quote pricing has a 15 s budget and returns pricing unavailable when it expires, leaving room for database work within the request deadline. Cache provenance is included in pricing audits; new metrics report cache outcomes and pricing budget expirations.
Fixed
-
Freeze persisted boundary conflicts from either successful RPC endpoint even when its peer errors. Preserve terminal proofs and final markers across held-settings waits, so subsequent business retries reuse the same evidence without chain reads; watcher completion reflects the actual handoff persistence result. Handoffs without a reusable terminal proof enter S and persist its backoff atomically with the version-checked transition.
-
Reference-
product sweep groups retain their last successful balances and sweep history for up to ten minutes when a refresh fails, while disabling stale signable calls. -
Deploy pause and resume requests retry transport errors and HTTP 408, 429, 500, 502, 503, and 504 responses; each attempt is signed fresh, delays are at least 1 second, and the operation is bounded by a 60-second deadline.
-
Kraken pricing uses order-book mid instead of a potentially stale last trade, retaining bid, ask and last in audit evidence. Checks with spreads above
max_are unavailable (deviation_ bps wide_spread), allowing failover without recording price disagreement. -
GET /v1/screens only the requested chain's treasuries concurrently with local list reads. The clear-verdict cache is removed, so snapshot and manual-list changes are visible on every decision.forwarders? sweepable= -
List endpoints for quotes, refunds, and forwarders use account-scoped indexes, and refunds load in one query; they scanned whole tables as history grew. Heartbeat reads use a timestamp index while preserving timestamp ordering.
-
Database pool acquisition timeouts and closed pools now return
503 unavailableinstead of500 internal_error, allowing retries without saving an idempotency response.
Removed
- Breaking: the admin endpoint
POST /v1/and the account'sadmin/ recording/ resume payment_field. Recording starts immediately after migrations; an incomplete payment-settings cutover refuses startup and must be completed on 0.9.x before upgrading. Historicalsettings. legacy legacyrevisions and their deposit bindings remain supported. - Breaking:
topup migrateno longer accepts--config; configuration validation usestopup config check. Migration refuses databases that require the old cutover before applying any migrations. Complete the cutover on 0.9.x first. - Breaking: the reusable Deploy workflow no longer accepts bootstrap_
maintenance; upgrades from 0.8.x are unsupported. - Breaking: route files no longer accept the legacy pricing section; use price. Coin Metrics is no longer a recognized source.
Security
- The service image's smokescreen and the PostgreSQL image's WAL-G and gosu are rebuilt with Go
1.27.2 and
golang.org/x/net0.60.0 (withgolang.0.57.0 for WAL-G), fixing CVE-2026-78667, CVE-2026-78669 and CVE-2026-97031.org/ x/ crypto
JS SDK (@phala/pay , @phala/pay-react , @phala/pay-server )
Added
- Browser
submitand automatic submission afterTransaction payWithWalletbroadcasts a retrieved quote's payment.<Checkout>inherits it. Server resources exposequotes.andsubmit Transaction deposit.Addresses. submit Transaction - Generated server error-code types include
address_andcapacity_ reached chain_; unknown future codes remain accepted.unavailable - Quote types expose
cancel_, including the browser's parsedrequested_ at ClientQuote. Checkoutexposes the HTTP status when the error came from a response.Error. status <DepositAddress onChange(state)>receives the public view after the first successful read and then once per content change, so integrators can update their UI without additional polling.
Changed
- Breaking:
@phala/exports the merchant client and API types, withpay- server Forwardernaming the API response type instead of contract pins. ReplaceForwarderimports withResponse Forwarder. Offline functions are exported only from@phala/; move pins, ledger, address, webhook and sweep helper imports to that entry.pay- server/ helpers - Checkout and React deposit-address polling use uniform ±20% jitter, pause in hidden tabs, and
read immediately on visibility regain.
Retry-Afterremains a minimum delay. <DepositAddress>uses an idle base interval ofmax(pollafter ten minutes without an observed change; any view change or visibility regain restores the normal interval and resets the idle window.Interval, 15000) - Checkout stops after three consecutive non-terminal 4xx responses other than 408/429 and
surfaces its existing error state. 404 still stops immediately as
invalid_.client_ secret - Breaking:
<Checkout>and<DepositAddress>are restyled with a neutral default theme (primary#171717, dark#fafafa; zinc greys; the page's font) and draw no frame, background, or width of their own: place them in your card or dialog, with the theme that matches it. Update your stylesheet:- Without
theme, defaults suit light backgrounds only; on dark hosts map every color variable, as an unmapped one keeps its light default. - The theme is these custom properties on
.pp-root[data-theme]:--pp-,color- surface -text,-text-muted,-border,-input,-primary,-primary-,foreground -focus,-success,-warning,-danger;--pp-font-family,--pp-,font- family- mono --pp-font-size(default14px;1emfollows the page);--pp-radius; and--pp-. Renamed, and an old name is silently ignored, so rename it in your mapping:control- height --pp-toaccessible- color- on- color- primary --pp-,color- primary- foreground --pp-tocolor- text- secondary --pp-,color- text- muted --pp-toborder- radius --pp-radius. Removed:--pp-root-border,--pp-,root- padding --pp-,root- max- width --pp-,root- background --pp-. The focus ring iscolor- background --pp-color-focus, no longer the primary color. The network and token choices' edges are the new--pp-color-input(default#8a8a93, dark#71717a, 3:1 or more against white,#f4f4f5, and dark backgrounds), no longer--pp-, which now draws only dividers and the QR code's frame: map it to a control-border token of yours that keeps 3:1.color- border - Classes are BEM-style blocks:
.pp-summary(__amount,__meta,__network,__test-mode; were.pp-amount,.pp-subtitle,.pp-badge),.pp-status(__icon,__text,__note,__countdown; was.pp-countdown),.pp-tabs(__tab,__panel; was.pp-tab),.pp-button(__icon,__detail; was.pp-wallet-name),.pp-qr(__code,__caption; was.pp-qr-panelaroundsvg.pp-qr),.pp-field(__label,__value,__text,__group; wasdt,dd,.pp-value),.pp-tx(__text,__hash),.pp-choices(__legend,__option,__input),.pp-payments(__item,__dot),.pp-intro(was.pp-subtitle), and.pp-skeleton..pp-root,data-theme,.pp-fields,.pp-notice,.pp-message,.pp-wallets, and.pp-copykeep their names;.pp-icon-labelis gone, asNetworkIconandAssetIconrender onesvg.pp-icon(withrole="img"when not decorative). - The status row's tone (
data-tone) shows only in its icon, on a neutral surface. Copy buttons are 32px icons with a 44px target, named "Copy …" byaria-label; their live region is empty until it announces "Copied" or "Copy failed". The transaction line has a copy button. The countdown is arole="timer". Live regions are always rendered and filled when needed:<DepositAddress>'srole="status"line is empty while connected, so that tests looking for no status should expect empty text. - Receive addresses and token contracts are shown in full in groups of four characters after
0x, wrapping only between groups and copying without spaces. Testing Library'sgetno longer finds them, as the text is split across groups: match the element'sByText(address) textContentinstead. Playwright text queries are unaffected. - Deposit network and token choices are native radios drawn as 44px buttons, with focus
distinct from selection. The QR code has the standard four-module quiet zone inside 8px of
white. Loading shows a placeholder; transitions stop under
prefers-; in forced-colors mode the selected choice and tab keep areduced- motion Highlightborder.
- Without
Removed
- Breaking:
@phala/no longer exportspay- server Webhook; useSignature Error Signature.Verification Error
Python SDK (phala-pay )
Added
- Quote objects expose
cancel_for deferred cancellation requests.requested_ at quotes.andsubmit_ transaction deposit_, with matching lower-level forwarding and regenerated clients in lockstep with JavaScript.addresses. submit_ transaction
Removed
- Breaking: the legacy
PhalaPayconstructor (deprecated in 0.9.2); use pins orPhala.Pay. from_ env() - Breaking:
phala_payno longer exports theTopupErroralias; usePhalaPayError. The low-leveltopup_remains available.sdk. Topup Error
0.9.2 - 2026-10-05
Fixed
- Chainlink sources omitting
rpc_group_bwithrpc_group: awhen the route's second RPC group is literallya(e.g.["x", "a"]) previously read one group twice, silently losing A/B independence. Upgrading reads two independent groups and may newly reject quotes asdivergent. Configurations settingrpc_group_bexplicitly are unaffected and need no action. - Sequencer uptime validation now checks the groups the service reads; previously it looked up
a/bliterally while the service resolved them as route aliases. Runtime is unchanged. Configurations that only validated under the old lookup may now be rejected byconfig check. Shipped staging, example, and examples/ configurations use non-alias sequencer group names and are unaffected.
Python SDK (phala-pay )
Changed
- The 60 resource method signatures that previously declared
request_deadlineandupgrade_now showtolerance **optionsinhelp()andinspect.; the accepted runtime keywords are unchanged. Unknown-keywordsignature() TypeErrormessages no longer include the method name prefix.
Deprecated
- The legacy
PhalaPayconstructor is deprecated and emitsDeprecation; it will be removed in 0.10.0. Configure pins or useWarning Phalainstead.Pay. from_ env()
0.9.1 - 2026-10-05
Upgrading from 0.8.x
- 0.9.0 cannot be deployed with Deploy. Upgrade 0.8.x directly to 0.9.1 and follow every step of
0.9.0's "Upgrading from 0.8.x",
including the pre-upgrade backup and the one-time
bootstrap_.maintenance: true
Fixed
- Deploy fetches
deploy/withdeadline. sh deploy/at the release commit, so release verification no longer fails before any change (0.9.0's Deploy stopped at "Verify the release"). A CI check keeps all three consumers fetching every scriptverify- release. sh verify-sources.release. sh - The 0.9.0 kit's example environment failed
topup config check; production examples now use Chainlink USDC/USDT routes, and PHA is explicitly limited to noncommercial rehearsal. CI validates every shipped environment config and example route template. - Cross-org callers can now pass
SENTRY_DSN; the example passes the maintenance key andbootstrap_input.maintenance
0.9.0 - 2026-10-05
Upgrading from 0.8.x
- Breaking: rollback is restore-only: no rollback to 0.8.3; restore required. Keep a pre-upgrade backup and follow the restore runbook; no 0.8.x rollback is supported.
- Breaking: migrate route
pricingtoprice, configure independent observation RPC groups and the Base sequencer gate, and replace restricted price sources. See the configuration migration. - Set up an independent maintenance key in attested
maintenance_keysand the deployment Environment before using planned upgrade admission. Follow maintenance key setup, including the explicit first-rollout bootstrap procedure. - Breaking: migrate JavaScript imports to the three packages using the import table below; review Python's typed transport errors and stricter validation before upgrading.
- Pin every SDK to the service version:
@phala/pay,@phala/pay-react,@phala/, andpay- server phala-paymust all use 0.9.0 with service 0.9.0.
Added
- Planned upgrade admission uses an audited, process-owned mutation pause with an expiring lease.
New mutations return
503 service_withmaintenance Retry-After; reads and in-flight work continue. Deployment health checks resume admission, with failure cleanup, automatic expiry, and a manual-clear runbook. Business pauses remain in effect. Deploy artifacts and the job summary record sampled first-failed/first-healthy downtime. - Independent maintenance signing keys and key IDs: attested
maintenance_keysauthorize only instance pause/resume; other admin routes return audited403 permission_. Full operator admin authority remains outside deployment CI.denied - Sentry business alerts for webhook backlog and stalled delivery, internal signer/egress failures, stale RPO heartbeats, overdue treasury/refund work, and ingress certificate expiry. Probes emit on state transitions with hourly reminders and log recovery without an event. Outbox alerts detect overdue processing and exclude known failing merchant endpoints; backlog warnings require at least two eligible endpoints. Staging synthetic alert commands and a production DSN preflight check support verification.
- Capacity alerts for pgdata/
observability disk usage at 75%/90%, pending WAL size/age, and stale probes. Data retention remains seven years, with documented safe disk-pressure response. - Licence-free
uniswap_v2_twapPHA/WETH × Chainlink ETH/USD pricing, pinned to one A/B-agreed Ethereum block. One-minute samples persist across restarts; prices require a continuous window of at least thirty minutes, with liquidity, spot divergence, freshness and sample jump guard rails and distinct price refusal alerts. PHA defaults use TWAP/Kraken: value at min(TWAP, current spot) × ETH/USD and compare current Uniswap spot × ETH/USD against current Kraken PHA/USD. Default spot/TWAP divergence is 3%, pausing faster moves; the stricter policy starts a new thirty-minute observation window. PHA remains staging-only because no second Allowed independent source exists; production stablecoin routes are unaffected. - Migration compatibility floors and checksum checks for N-1 rollback, verified against the
actual previous-release image. 0.9.0 establishes the compatibility-
ledger protocol with an explicit legacy bootstrap boundary; the restore-only declaration below governs this upgrade.
Changed
- Breaking: replace route
pricingwith explicitpricestablecoin sources or ordered volatile primary/check/FX lists. The legacy parser remains available for one migration window and emits the new shape; mixed schemas and restricted legacy sources fail validation. Replace Coin Metrics with reviewed Chainlink/exchange sources, configure mainnet A/B observation groups and testnet markers, and add the Base sequencer gate. Stablecoin defaults use only Allowed on-chain Chainlink data; production rejects Permission Required/ Prohibited sources. See the migration guide. - Price sources fail over independently within ordered roles and fail closed on insufficient evidence. Stablecoins credit exactly $1 only with a fresh in-band observation and no fresh depeg; volatile assets require two agreeing independent companies. Chainlink freshness allows the pinned heartbeat plus 600 seconds without weakening completeness, agreement or peg gates.
- WAL-G uploads, backup listings and restores have separate configurable deadlines and retry budgets. Timed-out recovery fetches abort recovery instead of promoting a partial restore.
- Ingress TCP connect/
client/ server timeouts are 5s/30s/30s. Production preflight enforces Sentry configuration; capacity thresholds use the shared business alert emitter. Deploy stages and network calls have elapsed-time deadlines with timeout diagnostics. - Expand-only migration
20261029000000_adds immutable observation history without changing payment tables or permissions. Scale migrationsuniswap_ twap 20261029030000–20261029030005add two cursor tables and five indexes without changing existing rows or constraints. Their compatibility ledger retains floor20261028000002: compatible protocol-aware binary rollback uses the previous config and retains observations. This does not permit rollback to 0.8.x. - Local SDK sandbox and CVM rehearsals use disposable TLS ingress with a run-scoped certificate, preserving HTTPS validation and normal system trust roots. Hermetic local Chainlink, Uniswap and exchange fixtures support price rehearsal.
Fixed
- Price RPC groups tolerate small pinned-read head regressions and fail over within a group.
- Image builds retry once without caches for recognized Actions cache transport failures only; cache export failures no longer fail builds, and diagnostics remain available.
- Backup health requires a current-timeline base backup and fresh WAL data, backlog and LSN progress; old backlog uploads cannot refresh recovery age. First base backups retry with capped backoff until successful.
- Failed critical restore checks block acceptance and unfreeze. An explicit administrator override requires a reason and is recorded atomically in audit history.
- Local restore drills use disposable Sepolia and Base Sepolia Anvil chains with distinct A/B RPC endpoints and contract fixtures. They enforce the 60-second RPO from failure to the last replayed committed marker, including upload latency; RTO includes unfreeze and a successful merchant API request.
- API body reads are bounded to 5 seconds and request processing to 25 seconds; health checks have independent capacity and a 2-second database deadline. API connections have a 30-second read idle timeout and a 60-second hard lifetime, bounding incomplete headers and keep-alive work.
- Scan all issued addresses, including retired addresses, in durable pages of 1,000. Finalized and confirmation cursors advance only after every address page commits. Reconciliation uses persistent rotating pages for historical credit, derivation, custody, and flush-link checks, with a bounded 4,000-entry derivation cache and concurrent query indexes. Post-restore row checks still traverse the complete ledger.
Breaking (operators)
- 0.9.0: no rollback to 0.8.3; restore required. No rollback to any 0.8.x release:
use the pre-upgrade backup and restore runbook. Route configuration and
SDK changes also prevent rollback. 0.9.0 is the first compatibility-
ledger protocol release; from 0.10.0 onward, real N-1 rollback is enforced unless an operator declaration and a raised compatibility floor require restoration. N-1 is always the latest stable release.
JS SDK (@phala/pay , @phala/pay-react , @phala/pay-server )
Added
- Opt-in
upgradeTolerancein@phala/for GET and idempotent POST retries across maintenance, connection failures, and gateway 502/503/504 for up to five minutes. Explicit deadlines and cancellation remain effective; keys and bodies stay fixed. Browser core checkout and React deposit address polling preserve their last view through outages and show a neutral reconnecting state.pay- server - Merchant server client at
@phala/: all OpenAPI resources, typed parameters/pay- server responses, verified quote/address results, two-variable Phala, frozen pins parse/encode, bound Ed25519 webhooks andPay. from Env() checkoutParamswith browser-exportedCheckoutParams. - Deadline-bounded transport with attempt timeouts, jitter, Retry-After, frozen POST idempotency, redirect refusal, lossless integer validation, cancellation and typed redacted errors.
- Pure
depositNetAmount/balanceDeltahelpers with monotone ledger convergence and strict validation, covering all five shared fixture groups. @phala/for keyless address, webhook and offline sweep builders. Existing server helper exports remain available;pay- server/ helpers Forwardernames the generated API forwarder whileResponse Forwarderretains its legacy contract-pins type.
Changed
-
Breaking: the JavaScript SDK is now three packages.
@phala/payis framework-free browser code,@phala/pay-reactcontains the React components andstyles.css, and@phala/contains the merchant client and server helpers. Migrate the removed entry points as follows:pay- server Old import New import @phala/pay/ react @phala/pay-react @phala/pay/ styles. css @phala/pay-react/ styles. css @phala/pay/ server @phala/pay-server @phala/pay/ server/ helpers @phala/pay-server/ helpers Install the new package explicitly at the service version.
@phala/payhas no React peer dependency or server/key- handling code. -
Breaking:
@phala/rejects browser-like environments before reading credentials. Move browser offline helper imports topay- server @phala/. Node.js >=20.3, Bun and Deno server runtimes are supported when fetch, AbortSignal.any and WebCrypto are available. The browser core entry and existing checkout props remain keyless.pay- server/ helpers -
Breaking: duplicate webhook signing headers and non-exact UTF-8 bodies are rejected. The legacy
Webhookexport is an alias ofSignature Error Signature.Verification Error
Python SDK (phala-pay )
Added
- Pins-based
PhalaPayconfiguration andfrom_env(), bound webhook verification, checkout parameters, and paginated address validation. Checkout handoffs bind the originating verified quote and revalidate its address. - Pure ledger helpers validate snapshots and merge cumulative refunds and reversals without floats or input mutation. Unknown webhook types are ignored while their raw objects remain available.
- Opt-in
upgrade_at client construction and per resource call for GET and replayable idempotent POST retries through maintenance, network/attempt timeouts, and gateway 502/503/504 (including HTML) for at most five minutes. Explicit deadlines stay hard limits, body/key stay fixed, and interrupts or replayed errors terminate retries. Interactive defaults remain 15 seconds per attempt, four attempts, and 60 seconds total.tolerance
Changed
- Breaking: transport failures are wrapped in typed SDK errors; update handlers that catch underlying transport exceptions. Diagnostics redact API keys and client secrets.
- Breaking: enforce immutable canonical pins, API key checksums, origin and response identity checks, and checkout handoffs from the originating verified quote. Redirects and invalid pagination are rejected; callers relying on previously accepted invalid inputs must migrate.
- Use one transport path for POST replay, explicit per-call controls, deadline and body timeouts, bounded jitter and Retry-After; borrowed transports retain their original ownership.
0.8.3 - 2026-10-04
JS SDK (@phala/pay )
Added
- Branded chain and token icons in Checkout and DepositAddress, with mainnet-family artwork on testnets
and neutral monogram fallbacks. Export
NetworkIcon/AssetIconfrom@phala/pay/reactand framework-freenetworkIcon/assetIconmarkup accessors from@phala/pay. Six SVGs are vendored from MIT-licensed web3icons, without a runtime dependency or injected styles.
Changed
- Checkout and deposit-address copy is shorter, with exchange withdrawal-fee guidance in the manual-transfer panel. Checkout defaults to QR when no browser wallet is discovered; payment tabs use single-line labels. Spacing is tighter with 44px touch targets.
- The React frame supports
--pp-root-border,--pp-,root- padding --pp-, androot- max- width --pp-overrides for embedding in host dialogs; frame defaults are unchanged.root- background
Fixed
retrieveQuoteandretrievetrimDeposit Address apiBase's trailing slashes in linear time; the previous/\/+$/pattern was quadratic on a long run of slashes (CodeQLjs/).polynomial- redos
0.8.2 - 2026-10-03
Added
- Live provider and stage elapsed-time diagnostics for read-only contract verification during deployment preflight, without printing RPC URLs or credentials.
JS SDK (@phala/pay )
Fixed
- Reject invalid webhook time options and malformed signed resource objects; preserve the default 300-second window, exact matching at zero, raw-body verification, and key rotation.
Python SDK (phala-pay )
Fixed
- Validate webhook time options and delivery timestamps consistently with the JS SDK; the low-level event verifier rejects malformed envelope fields instead of converting their types.
0.8.1 - 2026-10-03
Fixed
- CVM startup with inline Compose configs: topup uses the writable root filesystem required by Docker's config injection, while retaining its non-root user and other service hardening. The rehearsal now shares the release policy; Docker creation and recreation of every rendered variant, including the reference product, run in CI.
0.8.0 - 2026-10-03
Added
- Bounded HTTP request counters and latency histograms in signed admin metrics, including authentication, load-shed and read-only errors; documented operational service objectives.
- RPC operational alerts through the existing Sentry integration, with a one-minute group outage threshold and an RPC health runbook. Durable metrics expose their last successful refresh time.
Changed
-
Encode metrics with the pinned standard Prometheus client and enforce merchant/
client- secret quotas with the existing pinned governor library, preserving tenant/mode and joint admission. -
Separate treasury ownership proofs, tenant-scoped reads and periodic work into focused modules.
-
Quote lists batch deposit and pending-payment reads after scoped pagination, keeping detail payment selection, tenant/mode isolation, filters and cursor semantics.
-
RPC review and reorg replay queues use concurrent partial expression indexes keyed by chain and recovery epoch; the superseded review index is removed. Normal Deploy migration retries verify and rebuild interrupted invalid queue indexes without database shell access; concurrent index work runs after the atomic payment-settings cutover transaction.
-
Global request overload returns the shared JSON
503 unavailableerror withRetry-After: 1, request identifiers and tenant cache protection. -
OpenAPI includes shared merchant
403and merchant/admin503errors, documents required429and optional503retry delays, and describes tenant response cache protection.
Fixed
-
A failed RPC metrics refresh retains the previous snapshot and allows recovery to continue.
-
RPC acceptance and readmission now retry only transient failures within explicit probe attempt and deadline bounds, honoring throttling delays. Capability checks share one finalized snapshot so redundant tagged reads cannot spuriously reject a load-balanced endpoint's regressing head. Persisted head floors, chain/genesis identity and contract validation remain mandatory.
-
topup rpc checkreserves stdout for JSON and reports sanitized member/probe failure reasons on stderr, which the Deploy preflight now surfaces. A capability probes require an unsplit address-less 2 000-block log window; B probes verify recent addressed logs. -
Staging has keyless Sentio A backups on both chains and ethPandaOps B on Sepolia, with endpoint/
operator evidence, distinct company domains and shared quota budgets. Base Sepolia B stays singleton after rejecting Coinbase's pruned genesis and removing Pocket's unverifiable Supplier independence; the Phala Cloud template routes are unchanged. -
Member probes reject same-height snapshot/
anchor/ read hash conflicts and persistently freeze the chain when finalized validation detects a conflict, including early snapshot rejection. Owner recovery uses fresh full-probe deadlines for each member and an independent full operation deadline for anchor agreement, allowing healthy low-rate providers to complete route verification.
Reference product
- Replace the HTTP development server with pinned Starlette and Granian, bounded connections and workers, streamed body limits, request deadlines, uniform application transport errors, and graceful shutdown. Bound worker-accepted connections to 32 with a five-second total header-read deadline after acceptance; excess connections wait in the OS listen backlog configured at 128 (Granian's minimum), rather than entering the worker. Outbound SDK calls share a 25-second operation deadline without automatic retries; a single supervised worker has a 35-second shutdown limit within the container's 45-second grace period. The demo forwards TanStack Query cancellation to deadline-bound fetches and uses a native network select to avoid dynamic inline scrollbar styles.
Security
- Tenant and credential HTTP responses, including public payer views, idempotent replays,
authentication errors and restore rejections, send
Cache-Control: no-store. - Breaking: live reference products now require pre-verified pinned webhook keys; unpinned attestation key fetches remain available only in test mode.
- Price-provider responses are bounded while streaming, and API ingress and service containers have bounded resource and privilege exposure.
- Database sessions use role-specific time budgets; service shutdown bounds task draining, advisory-lock cleanup, and pool closure, including read-only restore instances.
JS SDK (@phala/pay )
Changed
- Breaking: React components require an explicit
@phala/import. Removedpay/ styles. css appearance.; set thevariables --pp-*custom properties in your stylesheet. Components no longer inject inline styles, allowingstyle-src 'self'in integrations. The demo permits only its own CSS and the exact hash of Radix's fixed scrollbar stylesheet. - Public reads accept a composable
AbortSignaland a request deadline (10 seconds by default). Breaking: Node.js 20.3 or later is required for the standardAbortSignal.anyAPI. Sessions and React components abort active reads on destruction or unmount.
Fixed
- Checkout calls
onSuccessafter a local expiry followed by credit, once per quote and notification type. DepositAddress uses native keyboard-accessible network/token radio groups.
Python SDK (phala-pay )
Changed
- Regenerate the client with shared forbidden and unavailable error responses.
0.7.0 - 2026-10-02
Changed
- Breaking: RPC configuration now requires explicit
chain.and therpc_ groups: { a: ..., b: ... } rpc_groups,rpc_companiesandrpc_budgetsregistries. The old provider list/flat registry is rejected, including a third id that 0.6.0 silently ignored. Reviewed provider companies must be disjoint between A and B; repeated URL templates with different sealed credentials are allowed within a group. - RPC groups select in-process with bounded failover or weighted round robin, shared account/key admission, refused redirects and no cache. Every real member send retains the existing usage counter. Fixed numeric log windows commit evidence, review coverage and progress atomically.
- Breaking: migrate the database before starting 0.7.0: persisted hash watermarks, configuration acceptance, historical review coverage and audited recovery are required. Legacy height-only cursors need an A/B-agreed hash anchor. Rollback requires the stopped-service recovery procedure in the RPC runbook; configuration rollback never lowers a watermark.
- RPC company aliases additionally use pinned PSL registrable-
domain evidence; accepted A/B role/group ids cannot be renamed or swapped. Receipt/ head/ nonce evidence is member-pinned, and typed decoding participates in failover. Dense-window deadlines account for real verification work, recursive log splits, factory checks and shared quotas; pinned sends retain the operation deadline. Same-height latest/safe reorgs replay through the production poll, recording deposits below prior confirmation progress without skipping unread ranges. Recovery probes isolate member heads and validate existing deposit and flush ledgers. Singleton historical replay and nonfinal reorg replay retain durable progress; authorized numeric- watermark recovery isolates old branch evidence in its audit epoch.
0.6.0 - 2026-10-02
Added
- Per-account payment settings,
GETandPOST /v1/(docs/payment_ settings integration. md §1.9, docs/ design/ payment- settings. md): per mode, the chains and assets the account accepts, a stricter confirmationsper chain, and its terms per asset (quote window, spread, two-sided tolerance, minimum credit, deposit bounds, refund floor) within the operator's bounds, plus one customer's quote creations per minute.chains, when sent, replaces the list; writes are last-write-wins.availablelists the operator's catalog of the mode with its defaults, bounds, and each chain's status. Each change has a newrevisionand is announced aspayment_, an account event delivered to every endpoint. Reading needssettings. updated account.read; writing needsaccount.write, which a restricted key never holds. rejecteddeposits carryrejection_for a routed token the payment settings the deposit is bound to do not accept; it is refundable like any rejected deposit.reason: "asset_ not_ accepted" - Error codes
asset_(a quote or deposit address of an asset the account does not accept) andnot_ accepted payment_(after a service restore, until the merchant reconfirms its settings).settings_ unconfirmed - A quote carries
terms, the terms it was issued with, which it keeps whatever the settings say later.GET /v1/configaddsquote_, and each assetcreations_ per_ customer_ per_ minute min_anddeposit_ atomic quote_.amount_ decimals - Admin:
GET /v1/, with each mode's payment settings and theadmin/ accounts/ {account} legacyrevision the 0.6.0 cutover bound its earlier deposits and quotes to, andPOST /v1/, which lifts the 0.6.0 cutover's recording hold. The admin deposit view names the deposit'sadmin/ recording/ resume settings_orrevision settings_hold.
Changed
- Breaking: an account accepts nothing until it configures its payment settings. A treasury no
longer enables a chain by itself, and a routed asset is no longer accepted by default: quotes and
deposit addresses answer
400 asset_,not_ accepted GET /v1/configlists no asset, and a payment to an existing address isrejected(asset_until the merchant lists what it takes.not_ accepted) GET /v1/configis the account's effective payment config: the accepted assets on chains with a treasury, with the account's terms. - Breaking: a deposit is governed by the payment settings current when it is recorded, or, for a valid payment of a quote, by the quote's terms; a change applies to deposits recorded after it. A deposit waits for the stricter of its bound confirmation and the chain's current floor; a stricter confirmation no longer applies to deposits already recorded. A payment the receipt corrects to the quote's asset waits for the quote's confirmation too.
- A chain's confirmation floor is that of its current route versions: a new version may raise it,
and an earlier version keeps its own value only for the terms of what it governed. A quote's
termsare always the stored ones,confirmationsincluded; a deposit's refund floor, when no accepted terms govern it, is the default of the route version it was recorded on. An unloaded bound version fails closed rather than using the current default. - Breaking: route files replace
quote:andlimits:with amerchant:section of each term's operator default and bounds (quote_,ttl_ seconds quote_spread_bps,quote_,tolerance_ bps min_amount,min_,deposit_ atomic max_,deposit_ atomic min_), with code ceilings; quote amount decimals becomerefund_ atomic asset.;quote_ amount_ decimals quote.is removed (now per account and mode, default 10, at most 60). Every environment's route files are converted.max_ creations_ per_ minute - Breaking: after a restore from backup, every account's payment settings are
helduntil the merchant sends its complete configuration withPOST /v1/(payment_ settings chainsrequired, and nothing of the restored settings carried over); deposits recorded meanwhile wait, and the reconfirmation binds them. No imported delivery lifts the hold. A delivered outcome stands once the chain shows the transfer it names (account, transaction, recipient, token, sender, and amount; otherwise the deposit is held for reconciliation): a rebuilt deposit with an importeddeposit.rejectedstays rejected, and a delivered credit is never rejected or bounded. A sanctions hit on a delivered credit keeps the credit, raisesTopup, blocks refunds (including pending refunds from being marked paid or completing verification), and keeps its forwarder out ofDelivered Credit Sanctioned GET /v1/. Delivered rejection identity contradictions also appear inforwarders? sweepable GET /v1/for reconciliation.admin/ restore - Breaking (operators): the upgrade to this release is a cutover (docs/
architecture. md §14): topup migrate --config FILEbinds every existing deposit and quote to the 0.5.0 model in one transaction with the schema change, serialized through commit by an advisory lock, after loading the configuration (the compose'smigrateservice mounts it),topup runrefuses to start before it, and recording stays held until the accounts are configured andPOST /v1/.admin/ recording/ resume
Removed
- Breaking:
confirmation_(thepolicies GET /v1/accountfield) andPOST /v1/account: a chain's stricter confirmation is part of the payment settings.
Python SDK (phala-pay )
Added
pay.andpayment_ settings. retrieve() .update(chains=, quote_(creations_ per_ customer_ per_ minute= ) Topup,Client. get_ payment_ settings update_), and the generatedpayment_ settings Payment,Settings Object Update,Payment Settings Request QuoteTerms, and catalog models. The account export writespayment_.settings. json PaymentandSettings Status RejectionReasonname the documented values.
Removed
- Breaking:
pay.andaccount. update(confirmation_ policies= ) Topup, the generatedClient. update_ account ConfirmationandPolicy Updatemodels, andAccount Object Request Account.Object. confirmation_ policies
0.5.0 - 2026-10-01
Added
- The customer's view of a deposit address (
GET /v1/,deposit_ addresses/ {id}? client_ secret= … Client) carriesDeposit Address typical_on each network: the typical time from paying to the credit at the account's confirmation for that chain, ascredit_ seconds GET /v1/configand the payer's view of a quote report it, so the page no longer assumes Ethereum's 30 seconds (Base's depth 3 is about 7 seconds, asafepolicy about 5 minutes, afinalizedpolicy about 15). It is required: a chain's routes share one confirmation floor, so every network has one value.
Changed
- Breaking: the service and both SDKs share one version from this release, released together
by one
v<version>tag:@phala/payandphala-paygo from 0.4.0 to 0.5.0. Use the SDK version equal to your operator's service version (docs/integration. md §5.9); the SDKs' earlier releases stay in their frozen changelogs in sdk/jsandsdk/python. - OP-stack chains (OP Mainnet, Base, Base Sepolia, OP Sepolia) credit at a depth on the sequencer's
unsafe head, as Ethereum does, by owner decision (design D1): the family default is 3 blocks,
about 7 seconds after paying (
GET /v1/configreportsconfirmations: "3"andtypical_), instead ofcredit_ seconds: 7 safe, about 5 minutes. This is a product-risk choice: Base reports a single reorged L2 block ever, and none after batching to Ethereum. As on Ethereum, the per-accountmax_cap bounds credit that is not final, and the finality watch sendsunfinalized_ credit deposit.reversedwhen a reorganization proves the payment replaced (its nonce spent by another transaction, or another transfer at its receipt position at finality). A payment removed with its nonce unspent is not reversed: it stays credited and not final, holding its share of the cap, and raisesTopupafter an hour. Staging's Base Sepolia routes credit at depth 3.Deposit Pending After Reorg - Every route of a chain must resolve to the same
chain., defaults included: a route set whose routes of one chain disagree is refused at load and byconfirmations topup config check. Before, the first loaded route's value silently governed the scanner and the confirm step while the API could report another route's. - A route's
chain.and an account'sconfirmations confirmation_accept a depth on an OP-stack chain;policies safeandfinalizedremain as stricter values. A policy may be any depth deeper than the route's,safe(OP-stack only), orfinalized: from weaker to stricter, a depth,safe,finalized. A value the chain's family does not accept, such assafeon Ethereum, is400with its own message. - The head loop polls a chain whose route credits at a depth once per block of its family: every
2 s on an OP-stack chain, 12 s on Ethereum (
--head-still overrides it). An OP-stack chain crediting at a depth takes about six times the head polls and per-block log requests on provider A of one crediting atpoll- interval- s safe(deploy/README. md, "Measuring RPC usage"). - Breaking:
deploy.shgenerates the admin key with the Python SDK of its own release, whichscripts/pins with the other version files, instead ofversion. sh phala-pay==0.3.0. A pre-release publishes no SDK, so itsdeploy.shneedsTOPUP_.ADMIN_ PUBLIC_ KEY
Removed
- Breaking: Every backward-
compatibility path for an earlier release, in the service, the SDKs, the demo, and the documentation. An SDK works only with the service release of its own version (integration guide §5.9, "Compatibility"); no other pairing is supported.
JS SDK (@phala/pay )
Added
Client(Deposit Address. networks parse,Client Deposit Address retrieve): each network'sDeposit Address chain_id,address, andtypical_, the typical credit time at the account's confirmation on that chain.credit_ seconds Clientis exported.Deposit Address Network
Removed
- Breaking: Support for a service of another version:
parserequires each network'sClient Deposit Address typical_, so it refuses a v0.3.5 service's view of a deposit address.credit_ seconds
Fixed
<DepositAddress>told the payer "usually in about 30 seconds" on every network. WithclientSecretandapiBaseit now states each network's typical credit time from the address's public view ("usually in about 30 seconds on Sepolia and about 7 seconds on Base Sepolia"; 15 minutes under afinalizedpolicy). Before the view is read it names none: "credited at the market rate once it is confirmed on its network".
Python SDK (phala-pay )
Added
Client(Deposit Address Network. typical_ credit_ seconds topup_client): the typical credit time at the account's confirmation on the network's chain.
Removed
- Breaking: Support for a service of another version:
ClientrequiresDeposit Address Network typical_, so it refuses a v0.3.5 service's view of a deposit address.credit_ seconds
0.3.5 - 2026-10-01
Added
- Staging adds USDT on both chains:
phala-andcloud- sepolia- usdt- usd phala-, for Aave's testnet USDT (Sepoliacloud- base- sepolia- usdt- usd 0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0, Base Sepolia0x0a215D8ba66387DCA84B284D18c3B4ec3de6E54a, 6 decimals; Tether publishes no testnet USDT, and Aave's faucet contracts mint it to anyone, which the demo's mint button does from the visitor's wallet), valued at one dollar (pricing modestablecoin, guarded by Coin Metrics'usdtreference rate) with no quote spread, as USDC is.GET /v1/configlists it beside PHA and USDC, and a deposit address takes all three tokens. examples/, a mainnet route template for Tether's USDT on Ethereum (phala- cloud- usdt. yaml 0xdAC17F958D2ee523a2206206994597C13D831ec7), and the runbookdeploy/for Tether's fee switch and blacklist: a blacklisted forwarder still receives and is credited but cannot be flushed, a blacklisted treasury still receives flushes, and a nonzero fee makes every sweep deliver less than it credited.runbooks/ usdt- issuer- controls. md
Changed
- Compliance is each operator's responsibility: Phala Pay is software, and beyond the service's direct sanctions screening, KYC, KYT, and the Travel Rule are for each operator and its merchants to handle. The legal review that was to gate live mode for third-party merchants on Phala's instance is removed from the design (D12, former §17), the architecture, and the plan: Phala's instance serves only Phala Cloud.
0.3.4 - 2026-10-01
Added
- The payer's view of a quote (
GET /v1/,quotes/ {id}? client_ secret= … ClientQuote) carriesamount_credited: whilepayment_statusiscredited, what the payment credited in cents (the deposit'samount), which differs from the quotedamountfor a payment valued at spot (another amount, or paid late); otherwisenull. It also carriestypical_, the typical time from paying to the credit at the account's confirmation for the quote's chain, ascredit_ seconds GET /v1/configreports it, so the page no longer assumes Ethereum's 30 seconds.
0.3.3 - 2026-10-01
Fixed
- A provision completes on Phala Cloud. v0.3.1 and v0.3.2 waited for a new CVM to boot by its
instance_idincvms get, which Phala Cloud reports asnullin practice, so the one-command deploy's quick start timed out after 15 minutes with nothing printed, though the CVM was healthy, and Deploy's provision timed out the same way.deploy/now waits only until the CVM is settled with the new compose, in any status; that the CVM booted the compose is the attestation's to show, and the instance id comes from its event log'sphala- cvm. sh wait --unsealed instance-idevent (deploy/), as Deploy's DNS records already did. The quick start waits for no instance id, as it is served at its gateway domain; a custom domain waits for the attestation of its gateway's compose and reads the TXT record's instance id from it.phala- cvm. sh instance-id - The one-command deploy binds a custom domain's gateway upgrade to the compose it rendered, as
Deploy does: the attested app-compose must carry that compose and the kit's pre-launch script,
and both the compose hash the API reports and the event log's must be that app-compose's hash,
so another update's compose fails instead of being accepted as "a new hash". The binding is
deploy/, whichattested- compose. sh deploy/now uses too; it also requires the event log to name that hash exactly once.verify- attestation. sh - The one-command deploy prints the CVM id, the URL, and how to finish or remove the CVM on every exit once the CVM exists, Ctrl-C included, including a step that fails or times out later.
- The one-command deploy refuses an instance name the Phala Cloud workspace already has, before
writing anything (an admin seed included), with how to find, finish, or remove that CVM: its
recorded
cvm-idis a file in the directory it ran from or the chosen environment directory, which a run from elsewhere cannot see. It fails closed: a CVM list it cannot read as one complete page of CVMs (page 1 of 1, or of 0 for no match; every match on it, its total the items' count; each item's name and app id) is refused, never read as "no such name". - The one-command deploy writes the sealed env file on tmpfs (
$XDG_RUNTIME_DIR) where the session has one, mode 0600, and shreds it (whereshredexists) and removes it on every exit. - The one-command deploy generates an admin key with the Python SDK pinned to
phala-pay==0.3.0, the version this release documents, as does the guide'suvxcommand; its messages name the release's locked CLI (kit/deploy/phala) and link the guide at the release's tag.
0.3.2 - 2026-10-01
Added
- A one-command deploy,
curl -fsSL https://(docs/pay. phala. com/ deploy. sh | bash self- hosting. md, "One-command deploy"). Each release publishes deploy/deploy.shas its attested assetdeploy.sh, set to deploy that release and listed inSHA256SUMS, whichdeploy/verifies; pay.phala.com redirectsverify- release. sh /deploy.shto the latest release's asset and/deploy/to that release's. The command is an HTTPS bootstrap, trusting pay.phala.com and Cloudflare as well as GitHub; the guide's high-assurance path verifies the script's attestation first. From the owner's machine it verifies the release (with the GitHub CLI 2.101 or later as Deploy does, otherwise againstv<version>. sh SHA256SUMSonly, which it says proves no provenance;--strictorPHALA_requires the GitHub CLI), asks for the settings and secrets or reads them withPAY_ REQUIRE_ ATTESTATION= 1 --non-from the environment, and provisions either the Phala Cloud template variant (a testnet quick start at the gateway domain) or the service variant on a custom domain, with the kit's render, preflight, route-mode check, locked CLI, and pre-launch script. It seals the secrets at provision from a mode 0600 file in a temporary directory it removes, prints none, and writes a generated admin seed only to the file the owner names. The Phala Cloud CLI runs in an empty directory, so ainteractive phala.tomlin the caller's directory cannot turn the new CVM into an update of another; the new CVM's id is recorded (cvm-id), and a rerun creates no second CVM. It prints the CVM id, the URL, a custom domain's DNS records, and the acceptance steps: a provision proves nothing about the instance's health. deploy/checks a Phala Cloud template compose and its env file, with the deploy form's values.preflight. sh --template - Preflight refuses an env file value with a
#, a quote, or surrounding whitespace: the Phala Cloud CLI reads the file as dotenv does and would seal another value (alpha#bravoasalpha). - SDK releases are never marked Latest, and stable service releases always are, so
releases/latest(andhttps://) is the service's; the Release workflow checks that the URL serves the new release'spay. phala. com/ deploy. sh deploy.sh.
Fixed
deploy/accepts a CVM whoseverify- attestation. sh allowed_envsleaves out a sealed name, such as the optionalSENTRY_DSN. The Phala Cloud CLI, and the template's deploy form, put inallowed_envsonly the names of the env they send, so a template CVM deployed without a Sentry DSN failed verification, which required exactly the compose's sealed names.allowed_envsmust now name only sealed names of the compose (compose-,policy. jq allowed_): any other name, the template'senvs_ violations DSTACK_included, is still refused. A name left out is unset. Preflight applies the same rule to the env file, and still refuses a required secret that is empty or missing.APP_ DOMAIN - The postgres-walg entrypoint refuses S3 storage unless both
AWS_andACCESS_ KEY_ ID AWS_are set, on every start of PostgreSQL and the backup job. Only an empty data directory listed the prefix, andSECRET_ ACCESS_ KEY /healthzreads only PostgreSQL, so a restarted CVM whose storage credentials were unset served without archiving and passed the acceptance upgrade.
0.3.1 - 2026-10-01
Fixed
- Deploy declares its one secret,
PHALA_(optional), so that an operator's repository in another organisation can pass it from a repository secret: GitHub supportsCLOUD_ API_ KEY secrets: inheritonly within an organisation or enterprise, and without it a called workflow's Environment secret resolves empty (actions/runner#4453). v0.3.0 read only the Environment secret, so every caller passing no secrets failed with an empty key. A caller in Phala-Network's organisation passes secrets: inheritand keeps the key as the Environment's secret. An empty key now fails with the setup each caller needs (docs/self- hosting. md, step 5, with the trade-off of a repository secret). - Deploy runs when called at a release's annotated tag, not only pinned to its commit. A workflow
called at an annotated tag runs with
job.workflow_shathe tag object's SHA, so v0.3.0's Deploy, called atv0.3.0, refused its own release ("Deploy runs at e42120e…, not v0.3.0's commit 659bd26…").deploy/peels the tag to its commit and accepts either SHA. Pinning the commit SHA remains the recommendation.verify- release. sh VERSION DIR CALLED_AT - Deploy's provision completes on the unsealed CVM it creates. Without the storage credentials
PostgreSQL refuses to start, so app-compose fails by design and Phala Cloud shows the CVM as
error; provision waited forrunningand timed out after 15 minutes, before the DNS records and the sealing step. A provision now only creates the CVM: it waits until the CVM is settled (in_progressfalse) with the new compose and has booted (an instance id), in any status (deploy/), and redeploys the gateway's compose without the CLI'sphala- cvm. sh wait --unsealed --wait. It proves nothing about the CVM's health: the upgrade after the sealing is the acceptance step, and still requiresrunning,/healthz, the attestation, and the certificate evidence. - An upgrade retries the certificate evidence every 30 seconds for at most 10 minutes instead of
failing at once: right after a DNS change the domain can still reach the previous instance, or the
new one can still be obtaining its certificate.
deploy/bounds its TLS handshake to 30 seconds.verify- ingress- evidence. sh - Deploy names a new CVM after its run (
phala-pay-<environment>-,<run id> phala-): Phala Cloud refuses a duplicate name (pay- staging- product- <run id> ERR-01-004), so a provision beside a stopped CVM kept for rollback, as in a reset, failed. A CVM is identified by its id variable, never by its name.
0.3.0 - 2026-10-01
Added
-
Versioned releases. A
v<version>tag of amaincommit runs CI, then publishes the images to GHCR, built on GitHub-hosted runners with pinned Buildx and BuildKit, each with a GitHub build provenance attestation (phala-payand the reference product are reproducible;postgres-walghas provenance only), and a GitHub release whose assets areimages.json, the deploy kitphala-(pay- deploy- v<version>. tar. gz git archiveofLICENSE,deploy/, anddocs/), the Phala Cloud template's composephala-, andcloud- template. yml SHA256SUMS, each attested.deploy/verifies a release: its commit inverify- release. sh main's history, the checksums, and every asset's and image's provenance for that commit. -
Self-hosting without a fork: an operator's repository holds only its environment directory and a workflow that calls Deploy at a release (
docs/); Deploy runs the release's verify-self- hosting. md release. sh and requires its own commit to be the release's. The kit's deploy/phalaruns the Phala Cloud CLI with its dependencies locked (deploy/tools). -
The environment's
compose.yamlmay set only its documented settings (WAL-G's location, dstack-ingress'sDOMAIN, the RPC key names):render.shrefuses any other change, and every rendered image must be the release's or one the kit pins. -
Deploy sends the kit's reviewed pre-launch script (
deploy/, Phala Cloud's v0.0.20) on provision and upgrade, andphala- cloud- pre- launch. sh verify-requires exactly it.attestation. sh -
The Phala Cloud template variant (
deploy/render.sh --template): Phala's staging routes on a one-click testnet instance served at the app's gateway domain. Exactly the deploy form's values come from the CVM's env and are unattested: the admin public key and the origin's host, whichtopup runreads with--admin-andpublic- key- env --public-when the configuration leaves them out, and the backup location. A template instance has no restore-check path.origin- host- env -
postgres-walg starts PostgreSQL or a backup only with WAL-G's file backend or all three S3 settings well formed:
WALG_S3_PREFIXa valid bucket,AWS_ENDPOINTanhttpsorigin with a valid host and port, andAWS_REGION. -
Deposits carry
receipt_andlog_ index revision, the position and revision theiridis derived from, andblock_hashandblock_time(Unix seconds), in the object and everydeposit.*snapshot. -
Admin:
POST /v1/applies again, while frozen after a restore, a treasury change that applied after the restore point, from the merchant's delivery of itsadmin/ restore/ treasuries/ apply treasury.updated: only a delivery the service signed, of the restored pending change becomingactive, whose time-lock ended, and screened again when screening answers (a treasury a sanctions list names now is refused). It applies at the event'screated, audited in the same transaction, and its events are not sent again.POST /v1/reports such a change asadmin/ restore/ treasuries/ verify application_lost, and the treasury it replaced, receivedreplaced, asreplacement_lostwhen that change is sent in the same request asactive(both werediffers); both arematchesonce the change is restored. A change on a chain without a current route stays pending, as the time-lock leaves it. -
Admin: each result of
POST /v1/carriesadmin/ restore/ events reversed_depositfor adeposit.reversed:restored,recorded,address_unknown, orrescanned(the rescan recorded its position first; also a finding status ofGET /v1/).admin/ restore -
Deposits carry
replacesandreplaced_by(dep_…ornull), in the object and everydeposit.*snapshot: a deposit recorded for the transfer that took a reversed deposit's receipt position after a reorganization names that deposit, and the reversed one names it (see Fixed). Both arenullwhen the other deposit is in another account or mode. -
Webhook delivery honors a receiver's
Retry-Afteron429and503, in seconds or as an HTTP date: the retry waits at least that long, at most an hour. -
Admin:
POST /v1/answersadmin/ deposits/ {id}/ nudge 400 deposit_for a deposit the pump does not process (anything butunexpected_ state detectedorconfirmed); it was a silent no-op. -
Admin:
POST /v1/re-issues a quote given out after the restore point from the merchant's record of it (the address must be the one itsadmin/ restore/ quotes qt_id derives over the current treasury), backfilled from the restored cursor so a payment made to it is found. Its terms are the merchant's record, kept but never applied: a payment to it is credited at spot unless an imported, signeddeposit.creditedcarries its credit, and itsexpires_atis the restore's detection at the latest. Aclient_secretthe service issued for the quote is kept, so the payer's page reads it again;POST /v1/takes one too.admin/ restore/ deposit_ addresses -
Integration guide §2.3, obligation 6: keep each webhook delivery as your receiver got it, once per
webhook-id, in the transaction that applies it: the raw body bytes and thewebhook-id,webhook-, andtimestamp webhook-headers. After a service restore only a delivery the service signed is imported (§5.12), so a parsed or re-serialized event cannot prove a credit; keep every quote and deposit address response whole too, itssignature client_secretstored like a credential. -
The reference product (
deploy/product) keeps a webhook inbox (each verified delivery as received, with its processing state, committed with its ledger effect; a redelivery with another body keeps the first and is logged) and each quote and deposit address response it gets, client secret included, in its ledger, now mode 0600.python -m reference_(from the ledger, read-only) orproduct export- restore- records fetch-(from its account API,restore- records GET /accounts/, signed with the driver key) prints them as the bodies ofrestore- records POST /v1/,admin/ restore/ treasuries/ verify /treasuries/,apply /deposit_,addresses /quotes, and/events;--sincekeeps what was created or recorded from five minutes before the restore point on. The controlled restore drill runs the product's receiver and uses only what it exports, not events the drill made up; it now also restores a treasury change lost with the restore, an unvalued reversed deposit, and a reorganized deposit's revisions. -
Admin:
GET /v1/returnsadmin/ attestation? account=& livemode=& nonce= GET /v1/attestationof any account and mode, so the operator verifies a restored instance, where merchant keys are refused. -
Admin:
POST /v1/requiresadmin/ restore/ unfreeze quotes_reissued. -
Admin:
POST /v1/releases a deposit held because its transfer contradicts the delivered event imported for it (aadmin/ restore/ delivered_ credits/ discard contradictedfinding ofGET /v1/); the deposit is then valued from the chain.admin/ restore -
Deposits carry
final_at(Unix seconds;nulluntilfinal), when the finality watch found the deposit's block final, in the object and everydeposit.*snapshot. -
A path or method the API does not serve answers
404 resource_missingwith the error object (it had an empty body). The error object'sdoc_urlis optional in the OpenAPI schema, as in Stripe's; every error of the service still carries it. -
Staging's route
phala-is version 3 on the deterministic factorycloud- sepolia- pha- usd 0x45466D37587E6E46DC35eB96b74ba3D3b1E5b747(implementation0x49F2F1F1a25269Ea0C6FF2AB1C7B09dCBE9c5bA9), withconfirmations: 2. -
Staging adds a second test-mode route,
phala-: Circle's testnet USDC on Sepolia (cloud- sepolia- usdc- usd 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238, 6 decimals; https://faucet. ), valued at one dollar (pricing modecircle. com stablecoin) with no quote spread, so a $10 quote asks 10 USDC.GET /v1/configlists it beside PHA, and a deposit address takes both tokens. -
Staging adds Base Sepolia (84532), with the same two test-mode routes:
phala-(test PHAcloud- base- sepolia- pha- usd 0x1a6F260377e42ead1418C7C1afDFD5DE371A9284, 18 decimals, publicmint) andphala-(Circle's testnet USDCcloud- base- sepolia- usdc- usd 0x036CbD53842c5426634e7929541eC2318f3dCF7e, 6 decimals, at one dollar with no quote spread), on the same factory and implementation. Deposits there are credited at the chain'ssafehead (GET /v1/configreportsconfirmations: "safe"), typically about 5 minutes after inclusion. -
Launch hardening (docs/
design/ multi- tenant. md, "launch hardening" amendment): - Restricted keys:
POST /v1/api_keys {"type": "restricted", "permissions": [...]}issues appay_key holding only those permissions (ark_ {test, live}_ writeincludes itsread);api_keyobjects gainpermissions. Restricted keys can no longer be grantedaccount.writeorendpoints.write: keys, treasuries, webhook endpoints, webhook keys, and account settings need a secret key. POST /v1/: in live modeaccount/ webhook_ keys/ roll expires_inis 172800 (48 hours) to 604800;0is refused there. The default is 172800 (was0). The roll'saccount.updatedis signed by the retiring key too, whenever it is delivered.- Treasuries gain
crediting_pausedandcrediting_;paused_ by POST /v1/andtreasuries/ {id}/ pause /resume(secret key), and the adminPOST /v1/, hold deposits to every forwarder over a treasuryadmin/ accounts/ {account}/ treasuries/ {treasury}/ pause| resume {reason} pendingwithoutdeposit.crediteduntil resumed; each change istreasury.updated.
- Restricted keys:
-
Ledger correctness (docs/
design/ multi- tenant. md, "ledger correctness" amendment): - Deposits carry
amount_refunded(the cents ofamountsucceeded refunds take back, pro rata to the refunded tokens, rounded down, cumulative) andamount_reversed(amountoncereversed), in everydeposit.*snapshot. Balance rule: a deposit nets toamount - amount_refunded - amount_reversedwhilecreditedorreversed, 0 otherwise; merge snapshots per deposit (the later status and the larger amounts win) whatever the order. - A refund with a transaction attached (
mark_paid) can no longer be canceled (400 refund_); it isunexpected_ state failedwith the newfailure_reasonstransaction_(its nonce consumed by another transaction at finality) ordropped transaction_(never seen within 24 hours), after which a new refund can be requested. A deposit's reversal cancels only its refunds without a transaction.not_ found mark_paidtakesreceipt_, the paying log's position in the transaction's receipt, and the refund reports it, replacing the block-widelog_ index log_index.- Per account and mode, the credit of deposits credited but not final is capped
(
max_, 100 000 cents by default, set by the operator withunfinalized_ credit POST /v1/); a deposit past it staysadmin/ accounts/ {account} pendingand is credited once final.
- Deposits carry
-
API conformance with Stripe (docs/
design/ multi- tenant. md, "API conformance" amendment): - Business-state failures are
400(deposit_,not_ final deposit_,not_ refundable quote_,unexpected_ state quote_,payment_ received quote_,window_ closed paused,chain_frozen,treasury_not_set,treasury_,change_ pending treasury_,unchanged treasury_,unexpected_ state exposure_,cap_ exceeded deposit_,address_ cap_ exceeded webhook_,endpoint_ cap_ exceeded webhook_,endpoint_ disabled deposit_,address_ retired refund_,unexpected_ state transfer_,already_ used api_key_inactive,last_api_key);409is onlyidempotency_. The unused generickey_ in_ use conflictcode is gone. The admin API'ssignature_isreplayed 401. - A customer's quote-creation and deposit address rotation limits are
429 customer_(wasrate_ limit rate_limit); every429carriesRetry-After. - Every response carries
Request-Id: req_…, replacingx-request-id. - An
Idempotency-Keysaves the result of every request that started executing,500s included, and replays it; a request that failed validation (parameter_*), was rate limited, or met503 unavailableis not saved. Validation failures were saved before,500s were not. - Event
data.objectis rendered in the transaction of the change, not at the first delivery or read, and never changes. - Treasury events are
treasury.created(every proven treasury, pending or at once active),treasury.updated(a pending treasury took effect, or was replaced), andtreasury., replacingcanceled account.; stored events and endpoints'treasury. pending| updated| canceled enabled_eventsare renamed. - A quote's address salt is
keccak256(abi.(was taggedencode(account, client_ reference_ id, "quote", quote_id)) "lock"). - Admin paths:
POST /v1/andadmin/ reconciliation_ blocks/ {block_ key}/ lift GET /v1/(wereadmin/ reports/ daily reconciliation-andblocks report/daily); a route pause names its path parameter{route}. - The OpenAPI document
openapi.jsonis the merchant API only; the admin API isopenapi.(also served atadmin. json /openapi.). Each object'sadmin. json objectis a single-value enum.
- Business-state failures are
-
API vocabulary (docs/
design/ multi- tenant. md §16 PR 10, and the Stripe- conventions audit): the merchant's customer is client_everywhere (reference_ id POST /v1/quotes, quotes, deposits,GET /v1/, and the admin pathdeposits? client_ reference_ id= /v1/).admin/ accounts/ {acct}/ customers/ {client_ reference_ id}/ pause| resume -
Deposit
statusispending(recorded at the route's confirmation, being valued and screened, or held by asettlementpause),credited,rejected, orreversed; the new booleansfinal(its block is final) andswept(a finalizedFlushedevent after it moved its forwarder's balance) replace thedetected,confirmed, andsweptstatuses. The status filter takes the new values. -
A quote's
paymentis the sharedPaymentobject:statusseenorrecorded(wasfinal), withchain_idandasset;matches_quoteisnullon a deposit address. -
GET /v1/attestation'squoteistdx_quote(and so istopup attest's output). -
GET /v1/returns theadmin/ deposits/ {id} Depositwith anadminobject (state, route,transitions,events); the separate admin deposit shapes are gone. -
livemodeandmetadataare required on every object in the OpenAPI document. -
ForwarderFactorybounds the gas of every call a target makes (BALANCE_OF_GAS30 000 for a token'sbalanceOf,FLUSH_GAS200 000 for a forwarder'sflush), so a token whosebalanceOfreverts, burns gas, or returns short data, or a transfer or treasury hook that burns gas, emitsFlushFailedfor its own target only; aflushwhose gas cannot cover a call's whole bound reverts withInsufficientGas. The factory and implementation addresses change (deploy/).CONTRACTS. md -
Events carry
request: {id, idempotency_key}(the request that caused them;nullfor the service's workers), and every*.updatedeventdata.. New eventsprevious_ attributes refund.created,refund.updated(marked paid, canceled, succeeded, failed, metadata), andquote.canceled. -
Delivery health: webhook endpoints report
pending_,deliveries oldest_, andpending_ at last_attempt {at, status_code};GET /v1/eventstakesdelivery_successandtypes[]; the admin daily report listsfailing_older thanwebhook_ endpoints failing_(24).for_ hours -
Error objects carry
doc_url, the code's section of the API reference (https://phala- ), built with Redoc fromnetwork. github. io/phala-pay/ openapi.jsonand published frommain. -
GET /v1/api_keysandGET /v1/treasuriestakelimit,starting_after, andending_before;GET /v1/depositstakescreated[gt]andcreated[lt]besidecreated[gte]andcreated[lte], all compared at whole seconds. -
The OpenAPI documents have
servers,tags, and an example of every object and body. -
Restore mode (docs/
design/ multi- tenant. md §13, architecture §14): after a restore from backup the service is frozen until the operator has reconciled it. Every request with an API key answers 503 service_withrestoring Retry-After: 300, reads included, and no event is delivered and no deposit credited meanwhile. Merchants give the operator their records since the restore point (integration guide §5.12). Operators:GET /v1/andadmin/ restore POST /v1/(admin/ restore/ {api_ keys/ revoke, treasuries/ verify, webhook_ endpoints/ delete, deposit_ addresses, quotes, events, delivered_ credits/ discard, unfreeze} deploy/);runbooks/ restore. md treasuries/re-applies lost treasury cancellations and the merchant's crediting pauses and resumes. On a restore-check instance, writes other than these answerverify 503 service_(wasrestoring 503 unavailable). -
POST /v1/account {confirmation_requires, per chain, a confirmation stricter than the route's floor (a depth,policies} safe, orfinalized), applied to every deposit not credited yet;GET /v1/configreports the effectiveconfirmationsandtypical_, and the account lists itscredit_ seconds confirmation_.policies POST /v1/pauses the merchant's own quotes and deposit addresses; an operator's pause stays until the operator lifts it. Both announceaccount/ pause| resume {scopes: ["quotes"]} account.updated. -
GET /v1/balance(per chain and token, unswept and final unswept amounts),GET /v1/sweeps(finalizedFlushedevents assw_…objects), andGET /v1/forwarders(fwd_…, every issued address with itsfactory,salt,treasury,quoteordeposit_address, andsuperseded_at;sweepable=lists only forwarders safe to sweep, never one holding a sanctioned deposit or paying a sanctioned treasury).<token> -
GET /v1/quotesandGET /v1/refundslists. -
Deposit addresses carry
payments(the last 24 hours,seenwithin about a block, thenrecorded) and each create or rotation returns aclient_secret; with it and no API key,GET /v1/returns the customer'sdeposit_ addresses/ {id}? client_ secret= Client.Deposit Address -
Client, andQuote. livemode payment_status: "reversed". -
Treasuries through the API (docs/
design/ multi- tenant. md D10, §16 PR 7). POST /v1/returns an EIP-4361treasuries/ challenge {chain_id, address} treasury_(challenge message,nonce,expires_at; single-use, 10 minutes, or 24 hours for an address that holds code);POST /v1/treasuries {chain_id, message, signature}sets the chain's treasury in the key's mode when the signature is the address's EIP-191 signature, or a contract deployed at the address returns0x1626ba7efrom EIP-1271isValidSignatureatfinalizedon both providers (ERC-6492 and undeployed contracts are refused; the address is screened for sanctions).GET /v1/treasuries,GET /v1/, andtreasuries/ {id} POST /v1/. Atreasuries/ {id}/ cancel treasury(trs_…) haschain_id,address,kind(eoaorcontract),status(pending,active,replaced,canceled),effective_at,replaced_at,canceled_at, andcancellation_(reason requested, orsanctionedwhen a sanctions list named it at its effective time). Current treasuries are screened again daily; a listed one pauses the account'squotesandsettlement. Safe owners sign the challenge as a Safe message (EIP-712SafeMessage, the Safe{Core} SDK'ssignMessage), or approve it withSignMessageLib. A chain's first treasury and test-mode changes apply at once; a later live change applies after 48 hours unless canceled. New eventsaccount.,treasury. pending account., andtreasury. updated account.go to every enabled endpoint of the mode whatever itstreasury. canceled enabled_events. When a change applies, the chain's network of every deposit address moves to a forwarder over the new treasury; the old address stays credited and pays the old treasury. New errors:treasury_,proof_ invalid treasury_,challenge_ expired treasury_,challenge_ used treasury_,not_ deployed treasury_,sanctioned treasury_,change_ pending treasury_,unchanged treasury_, andunexpected_ state treasury_not_set. -
Quotes carry
treasury, the treasury their address pays. -
Admin
POST /v1/andadmin/ accounts/ {account}/ pause /resume{scopes, reason}: pause or resume scopes of a whole account in both modes, audited and announced asaccount.updated. -
Webhook endpoints managed by the merchant (docs/
design/ multi- tenant. md D11, §11, §16 PR 8): POST /v1/,webhook_ endpoints {url, enabled_events, description?, metadata?} GET /v1/(cursor pagination),webhook_ endpoints GET|POST|DELETE /v1/(webhook_ endpoints/ {id} disabled: true|falsedisables or re-enables), andPOST /v1/(awebhook_ endpoints/ {id}/ test webhook_event to that endpoint only). At most 16 per account and mode (endpoint. test 409 webhook_);endpoint_ cap_ exceeded urlishttpson port 443, or in test mode alsohttpon port 80. The object carrieslivemode,url,enabled_events(types or["*"]),status(enabled,disabled),disabled_reason(gone),description, andmetadata. -
Events API:
GET /v1/(a type, or a group such asevents? type& created[gt| gte| lt| lte] deposit.*; cursor pagination),GET /v1/events/{id}, andPOST /v1/(the same event to one enabled endpoint;events/ {id}/ resend {webhook_ endpoint} 409 webhook_otherwise). Events carryendpoint_ disabled actor(key_…,admin, orsystem) in the API and in webhook bodies, andpending_webhooksin the API: the audit log. -
Account events
webhook_,endpoint. created webhook_(withendpoint. updated data.), andprevious_ attributes webhook_. Account events (endpoint. deleted account.*,api_key.*,webhook_) reach every enabled endpoint of the mode whatever itsendpoint. * enabled_events, and a changed or deleted endpoint receives the event about itself first, at its previous URL. Permissionsendpoints.read,endpoints.write, andevents.readare enforced. -
Deposit addresses (docs/
design/ multi- tenant. md "Deposit addresses"), restored per the owner's 2026-09-21 requirement, with one address per customer for every supported token on every chain (the owner's 2026-09-28 decision, exchange practice). POST /v1/returns the customer's activedeposit_ addresses {client_ reference_ id} deposit_address(da_…), issuing it the first time and adding a network supported since;GET /v1/,deposit_ addresses/ {id} GET /v1/(cursor pagination), anddeposit_ addresses? client_ reference_ id& status POST /v1/, which retires it and returns the next version, a new address on every chain. The object carriesdeposit_ addresses/ {id}/ rotate livemode,address(the address shared by every network, ornullwhen a network's treasury, and so its address, differs),version,salt(keccak256(abi., no chain or asset),encode(account, livemode, client_ reference_ id, "deposit_ address", version)) status(activeorretired),created,retired_at,metadata(set on create by merging, updated byPOST /v1/, carried by rotation, and copied to each deposit to the address), anddeposit_ addresses/ {id} networks: per chain of the mode,chain_id,address,treasury, andassets(asset,contract,decimals, and an EIP-681payment_uriwithout an amount). A transfer of any supported token to an active or retired deposit address is credited at spot through the quote pipeline, in about 30 seconds; an unsupported token is rejected as at a quote's address. Its deposit hasquote: nulland the new fielddeposit_address, andGET /v1/depositsfilters bydeposit_address. A treasury change on one chain changes only that chain's address; the old one stays credited and its refunds are paid from the old treasury. New errors:409 deposit_(active addresses per account and mode, default 100 000 live and 1 000 test),address_ cap_ exceeded 409 deposit_, andaddress_ retired 429 rate_limitpast 10 rotations per customer per hour. New addresses are refused with409 pausedwhilequotesis paused, and with409 chain_frozenwhen every chain is frozen; a frozen chain gets no new network. Permissionsdeposit_andaddresses. read .writejoin the authorization table. -
POST /v1/(docs/account/ webhook_ keys/ roll {expires_in} design/ multi- tenant. md D11, §16 PR 6): the next version of the mode's webhook key signs every delivery, and the current one keeps signing beside it for up to 7 days ( 0stops it at once), so each delivery carries onev1aentry per key.GET /v1/accountlists the versions aswebhook_keys(version,expires_at); the roll is announced asaccount.updated. Needsaccount.write. -
livemodeon quotes, deposits, refunds, and/v1/config; events carryaccount(acct_…) andlivemode. -
GET /v1/account;GET|POST /v1/api_keys,GET|DELETE /v1/, andapi_ keys/ {id} POST /v1/(the old key works for up to 7 days;api_ keys/ {id}/ roll {expires_in} 0revokes it). -
Events
api_key.created,api_key.updated,api_key.revoked, andaccount.updated; every event records itsactor(an API key id,admin, orsystem). -
Stripe-style
metadataon quotes, deposits, and refunds (docs/design/ multi- tenant. md D15, docs. stripe. ): up to 50 string key/value pairs, keys of up to 40 characters without square brackets, values of up to 500 characters. Set it withcom/ api/ metadata metadataonPOST /v1/quotesandPOST /v1/refunds; update it with the newPOST /v1/quotes/{id},POST /v1/, anddeposits/ {id} POST /v1/refunds/{id}, which merge (""unsets a key,metadata: ""unsets all). Invalid metadata is400 parameter_withinvalid parammetadata[key]ormetadata. A deposit starts with a copy of its quote's metadata, so it arrives indeposit.credited'sdata.object. Objects and webhook payloads always carrymetadata({}when empty); theclient_secretview does not. Secret keys gaindeposits.write. Do not store sensitive information in metadata. -
Accounts and tenancy (docs/
design/ multi- tenant. md §14, D13, PR 3). The tenant is an account, acct_…. Its request signing key id is{acct_…}/v1, and the key is live or test: it quotes on the routes of its mode and reads only its mode's objects. Another account's object, or the same account's object in the other mode, answers404like a missing one. Webhook events go to every enabled endpoint of the event's account and mode. -
POST /v1/issues an account andadmin/ accounts {name, livemode, public_key, webhook_url} PUT /v1/replaces its key and webhook URL;admin/ accounts/ {account} POST /v1/pauses one customer. The admin deposit view carries the deposit'sadmin/ accounts/ {account}/ customers/ {customer}/ pause | resume accountandlivemode. -
Fast credit and reversal (docs/
design/ multi- tenant. md §4, D1). A route's chain.(a depth,confirmations safe, orfinalized, per chain family; default 2 on Ethereum L1,safeon OP-stack,finalizedelsewhere) sets when a deposit is credited: at the default,deposit.creditedis sent about 30 seconds after paying instead of about 15 minutes. Deposits are watched to finality. A transaction re-included in another block keeps its deposit and is followed; one proven dropped (its nonce consumed by another transaction), or whose transfer is missing from its final receipt, makes the depositreversedand sendsdeposit.reversed(event iduuid_v5(NS, "deposit.) when the deposit was reported credited or rejected. Claw the credit back as forreversed: " + deposit UUID) deposit.refunded. A quote the deposit completed opens again while its window lasts, otherwise expires withquote.expired.confirmations: finalizedkeeps the earlier behaviour. -
GET /v1/configassets carryconfirmations("2","safe", or"finalized") andtypical_; the admin deposit view carriescredit_ seconds receipt_andlog_ index final_at. -
POST /v1/refundsanswers409 deposit_for a deposit that is not final yet, so nothing is paid back for a payment that could still be reversed.not_ final
Changed
-
Breaking: the SDKs of this release are
@phala/pay0.3.0 andphala-pay0.3.0 (sdk/js/ , sdk/CHANGELOG. md python/ ): webhook keys only asCHANGELOG. md whpk_…, and the event envelope'sactorandrequest, a deposit's receipt position, revision, and block, and a client view'sconfirmationsrequired. -
Breaking:
GET /v1/attestationlists each webhook key'spublic_keyin Standard Webhooks' form,whpk_and the standard base64 of its 32 raw bytes, whichreport_databinds; it was lowercase hex, andstandard_is gone. Pin the keys in this form: both SDKs' verifiers take no other.webhooks_ public_ key topup attestprints them the same way. -
Breaking: a deposit's
receipt_,log_ index revision,block_hash, andblock_timeare required in the OpenAPI schema, andPOST /v1/refuses a deposit event whose snapshot lacks them;admin/ restore/ events reversed_depositis neveridentity_missing. -
Breaking: Admin:
POST /v1/andadmin/ restore/ events /treasuries/take event ids only asapply evt_…, as every delivery'swebhook-idis. -
Breaking: Admin: the deposit view and
nudgetake a deposit only by itsdep_…id; a bare UUID is404. The service's logs, and a nudge's audit subject, name deposits, events, refunds, and an outbox delivery's object by their prefixed ids (dep_…,evt_…,re_…), the form the API takes. -
Breaking for operators: the database enforces what the service reads as constraints (
20261023000000_): a deposit carries its transaction'scurrent_ invariants tx_fromandtx_nonceunless it is a reversed deposit restored from a delivered event, every event'sdataholds itsobject, and a deposit event's snapshot itsreceipt_,log_ index revision,block_hash, andblock_time. A row that breaks one failstopup migrate. -
Breaking: a client quote's
confirmations, and a client deposit address payment's, are required in the OpenAPI schema, an integer ornull, as the service always sends them. -
The reference product applies a
deposit.*event only with a valid deposit snapshot: adeposit.creditedwithout one is logged and credits nothing. -
Breaking for operators: Deploy is a reusable workflow that deploys a release (
version), not a Release images run (release_run_id), from the caller'senvironment_dir; Phala's instance deploys through "Deploy Phala's instance". Images are published only by the Release workflow, from a tag, tagged with the version. -
deploy/compares each chain with the committed reference deploymentcontracts/ verify- deployment. sh deploy/, which CI checks against a fresh build (contracts/ reference. json reference-), so it needs no Solidity build and runs from the kit.manifest. sh --check -
The example environment's header no longer names a value preflight refuses, so a filled-in copy that keeps its comments passes.
-
While the service is frozen after a restore, a merchant request without a well-formed API key answers
401 api_key_missingor401 api_key_invalid, as when not frozen (it answered503 service_): the key's form and checksum are checked first, without a database read. A well-formed key still answersrestoring 503 service_, reads included, and nothing is saved for itsrestoring Idempotency-Key. -
An
Idempotency-Keyolder than 24 hours is pruned in the background, not while aPOSTclaims its own key, so no request pays for pruning every account's keys; such a key is still free for any request. -
Breaking: A key rolling itself (
POST /v1/with the requesting key's own id) must keep working for at least an hour:api_ keys/ {id}/ roll expires_inunder3600, including the default0, is400 parameter_. A replay never returns the new key's secret, so an immediate self-roll whose response was lost locked the account out; now the old key rolls the new one (its id is in the replay) to recover. To stop the old key sooner, revoke it with the new key. Another key may still roll a key withinvalid expires_in: 0. -
Breaking: A
client_secretis{id}_, 64 lowercase hex digits aftersecret_ {nonce}{tag} _secret_(was 48), wheretagis the service's HMAC of everything before it. A forged or malformed secret is refused in memory (404) without touching the database or any budget, so forgeries can no longer throttle checkout polling; a genuine secret is limited to 120 reads per minute of its quote or deposit address, withRetry-After. -
Breaking: Route files no longer take
unit_decimals: credit is always USD cents, the API'samount. A route file that sets it is refused. -
Breaking: Open-quote caps are per account and mode only (design §12), set by the operator per account and mode (defaults: 1 000 open quotes, $50 000 of open quotes per account, $5 000 per customer in live mode; 100, $10 000, and $5 000 in test mode). There is no global cap, and test-mode quotes never count against live mode.
GET /v1/configreports the effective caps:max_open_quotesandmax_are new, andopen_ amount_ per_ customer max_is now the account's cap in the mode (it was the per-customer cap).open_ amount_ per_ account 400 exposure_also answers a quote pastcap_ exceeded max_open_quotes. Route files no longer takelimits..max_ open_ minor -
Breaking: Admin:
POST /v1/takesadmin/ accounts/ {account} limits {livemode, max_open_quotes, max_, and the admin account response carries the effectiveopen_ amount_ per_ account, max_ open_ amount_ per_ customer, max_ active_ deposit_ addresses} limitsof both modes. -
Breaking: Deploy runs one production deployment for both modes: every route's
livemodemust match its chain (live on a mainnet, test on a test network), and staging takes no live route (deploy/); production no longer requires every route to be on chain 1.check- route- modes. sh -
Breaking: while the service is frozen after a restore from backup, no API key authenticates, reads included: every request with a key answers
503 service_withrestoring Retry-After(only writes did). The restored database can hold a key you revoked after the restore point as valid; keys work again once the operator has revoked such keys again and unfrozen the service. A quote's or deposit address'sclient_secretread, the admin API, and/healthzare unaffected. A restore-check instance refuses every merchant key, whether or not the freeze is recorded yet. -
Breaking: Admin:
POST /v1/takesadmin/ restore/ events deliveries, each delivery as the merchant's receiver got it (webhook_id,webhook_,timestamp webhook_, and the rawsignature body), instead of bare event objects, and imports only deliveries whosev1asignature verifies with the account's webhook keys. -
Breaking: quotes and deposit address networks pay the account's treasury of the chain, set through the API, instead of the route's:
POST /v1/quotesis409 treasury_not_seton a chain without one, andPOST /v1/issues networks only on chains with one (deposit_ addresses 409 treasury_not_setwhen none has). Route files no longer havechain.treasury(a route file that still names it is refused), and the admin daily report dropstreasury_andbalance_ atomic treasury_.balance_ note -
Breaking: webhook delivery (§16 PR 8). Retries still continue until delivered (backoff capped at 1 h) and a failing endpoint is never disabled (owner decision);
410 Gonefrom the receiver disables its endpoint at once (disabled_reason: gone), announced to the account's other endpoints aswebhook_. A disabled or deleted endpoint's pending deliveries stop; resend them withendpoint. updated POST /v1/. Each endpoint has at most 4 deliveries in flight, slots go round-robin across endpoints, a failing endpoint is probed one delivery at a time after a backoff, and events are not ordered. Deliveries leave through an egress proxy (smokescreen) that refuses addresses that are not publicly routable.events/ {id}/ resend -
Breaking: the operator no longer manages merchants' webhooks:
webhook_urlis removed fromPOST /v1/andadmin/ accounts POST /v1/(an unknown field isadmin/ accounts/ {account} 400), andPOST /v1/and theadmin/ outbox/ {event_ id}/ replay topup outbox replaycommand are removed. Existing endpoints are kept and are now managed through/v1/.webhook_ endpoints -
Breaking: webhooks are signed with a key per account and mode (docs/
design/ multi- tenant. md D11, §16 PR 6), derived in the attested CVM at settlement/, instead of the one shared{acct}/ {live| test}/ v{n} settlement/v1key: an event signed for one account never verifies at another.GET /v1/now needs an API key (attestation? nonce= 401without) and returns{object: "attestation", account, livemode, webhook_keys: [{version, public_key, expires_at}], report_data, quote}, wherereport_data = sha256(len(nonce) ‖ nonce ‖ len(account) ‖ account ‖ livemode ‖ (version ‖ public_key)*);keyidandsettlement_are removed. Pin your account's key per mode and check the event'spubkey accountandlivemode. Test and live events are delivered by separate workers. -
Breaking: API keys replace RFC 9421 request signing for merchants (docs/
design/ multi- tenant. md D7, D8, D12, §16 PR 5). Send Authorization: Bearer ppay_sk_test_…orppay_sk_live_…; the key selects the account and the mode. A missing, invalid, or revoked key is401 api_key_missingor401 api_key_invalid, a rolled key past its expiry401 api_key_expired, and a live key of an account not enabled for live mode403 testmode_. Requests are limited per account and mode (100/s live, 25/s test, with a test-mode platform ceiling):charges_ only 429 rate_limit. -
Breaking: every
POSTis idempotent byIdempotency-Keyfor 24 hours per account and mode: a repeat of the same request replays the first response (Idempotent-), another request with the same key isReplayed: true 400 idempotency_(waskey_ reused 409), and a repeat while the first runs is409 idempotency_. A repeated quote creation now returns the samekey_ in_ use client_secretinstead of a new one. -
Breaking: accounts are created only by the operator:
POST /v1/returns the first secret keys;admin/ accounts {name, contact, due_diligence, charges_enabled, reason, webhook_url?} POST /v1/(wasadmin/ accounts/ {account} PUT) updates live mode, the restricted flag, the contact, or the webhook URL, and enabling live mode returns the first live key;POST /v1/issues a recovery key. Customer pauses takeadmin/ accounts/ {account}/ api_ keys {livemode, revoke_existing, reason} livemode. -
Every issued address is scanned at every block, not only open quotes' addresses: a late, repeated, or wrong-amount payment, or one to a persistent address, is credited at the route's confirmation (about 15 seconds after inclusion on Ethereum at depth 2) instead of at finality, and shows as
seenin the quote'spaymentmeanwhile. RPC usage no longer grows with polling (docs/architecture. md §8): one eth_blockNumberper block time and oneeth_getLogsper new block per chain, whatever the number of addresses; the admin-signedGET /v1/reports the calls per provider, chain, and method (deploy/admin/ metrics README. md, "Measuring RPC usage"). -
Breaking: refunds are paid by the merchant (docs/
design/ multi- tenant. md D5, PR 9), in BTCPay's two-step payout flow. POST /v1/refundscreates apendingrefund of a final deposit and reserves its amount; its destination is screened for sanctions (400 destination_,sanctioned 503 unavailablewhen screening cannot answer). Pay it from the refund's newtreasuryfield (the treasury of the deposit's own address, not the account's current one), then attach the transaction withPOST /v1/. At finality on both providers, arefunds/ {id}/ mark_ paid {transaction_ hash, log_index?} Transferof the deposit's token from that treasury to the destination for exactly the amount, in a log no other refund holds, makes the refundsucceededand sendsdeposit.refunded; anything else makes itfailedwith afailure_reason, releases the reservation, and sendsrefund.failed(Stripe's event; event iduuid_v5(NS, "refund.failed:" + refund UUID),data.objectthe refund).POST /v1/cancels a pending refund; a reversed deposit cancels its pending refunds. The Refund object gainsrefunds/ {id}/ cancel treasury,failure_reason, andlog_index, renamestx_hashtotransaction_hash, and itsstatusis Stripe'spending,succeeded,failed, orcanceled. New409codes:refund_,unexpected_ state transfer_. The operator'salready_ used POST /v1/andadmin/ refunds/ {id}/ approve /recordare removed, and the daily report'srefunds_counts the new statuses.by_ status -
Breaking: the service sends no transactions (docs/
design/ multi- tenant. md §5, §13, PR 4). Anyone, usually the merchant with its own wallet or Safe, sweeps forwarders with the permissionless factory's flush(treasury, salts, token)and pays the gas. The finalized scanner indexes the factory'sForwarderCreated,Flushed, andFlushFailedevents for the account's own(address, treasury)pairs, whoever sent them, and a final credited deposit becomessweptonce a finalizedFlushedevent follows it. AFlushFailedtarget keeps its balance and its deposits staycredited. Reconciliation compares every active forwarder's finalized balance with its deposits minus its finalized sweeps; a mismatch freezes crediting on the chain (409 chain_frozen) until the operator lifts it. -
Breaking: attestation binds only the settlement key:
GET /v1/attestationdropsoperators,report_dataissha256(nonce ‖ settlement_, andpubkey) topup attestdrops--routeand--operator-and itskey- version operators,operator_keyid, andoperator_addressfields. -
Breaking: route files drop
chain.,operator_ key_ version chain.flush,limits., andmin_ flush_ atomic alerts.(a credited deposit waits for its merchant's sweep); the pause scopestuck_ after_ s. credited flushis gone. The daily report dropsflush_planning, and itsunflushed_is deposits not reversed minus finalizedbalance_ atomic Flushedamounts. -
Breaking: route files drop
productand requirelivemode, checked against the chain. -
Breaking: products are gone.
POST /v1/,admin/ products PUT /v1/, andadmin/ products/ {slug} POST /v1/are replaced by the account endpoints above; the quote address salt's first input is theadmin/ products/ {slug}/ accounts/ {account_ id}/ pause | resume acct_…id instead of the product slug. -
A quote's
account_idholds 1 to 200 characters (was 255 bytes). -
Breaking: a deposit id is
uuid_v5(NS, "{chain_, the transfer's position among its transaction's receipt logs (0 for a plain token transfer), instead of the block-wideid}: {tx_ hash}: {receipt_ log_ index}") log_index, so a re-included transaction keeps its id.log_indexandblock_numberstay on the deposit as evidence and change when the transaction is re-included. -
Deposit
statusgainsreversed; the quote'spayment.statusfinalnow means recorded at the route's confirmation, and the payer'spayment_statusconfirminglikewise. -
POST /v1/andproducts/ {p}/ accounts/ {ext}/rate-locks POST …/deposit-addresscreate the account when it does not exist, so a quote or an address is one call;POST …/accountsis no longer required. Reads (GET, rotate, cancel, pause) of an unknown account still answer404. -
DepositResponse(deposits, deposit, and support lookup) carriesexternal_id, the account of the receiving address, andprice_source(lockorspot). -
PUT /v1/(administrative API) replaces an issued product's verification key and webhook URL, whichadmin/ products/ {slug} {public_key, webhook_url, reason} POST /v1/refuses withadmin/ products 409. The key id stays the route'sdestination.. The cut is immediate: the old key stops verifying when the change commits, with no overlap. Theproduct_ kid auditrow (product.update) carries the reason and the replaced values; a repeat with the stored values changes nothing. An unknown slug is404, an unrouted slug or invalid value400. -
POST /v1/lifts a reconciliation block (admin/ reconciliation- blocks/ {block_ key}/ lift {reason} chain:{chain_id}oraddress:), which production could not do without a database owner session. Lifting is manual: the reconciler blocks again if the finding still reproduces. The{address_ id} auditrow (reconciliation_) carries the reason and the removed block; a repeat returns the first lift, and a key that never blocked isblock. lift 404.GET /v1/lists the activeadmin/ report/ daily reconciliation_.blocks -
POST /v1/queues an existing webhook event for delivery again with the same id and payload (audited asadmin/ outbox/ {event_ id}/ replay {reason} outbox.replay); a repeat while the event is due changes nothing. The product-signed support lookup (GET /v1/) lists each deposit's webhookproducts/ {p}/ deposits? tx_ hash=| address=| lock_ ref= events(id,event_type,created_at,delivered_at). -
GET /v1/returnsadmin/ report/ daily exposure_minor, the global open rate-lock credit in destination minor units (#94). -
Breaking: webhooks are Stripe's Event object,
{"id": "evt_…", "object": "event", "type", "created", "data": {"object": …}}, wheredata.objectis the deposit (deposit.credited,deposit.rejected,deposit.refunded) or the quote (quote.expired, which replacesrate_) as the API returns it, rendered at the first delivery attempt. Thelock. expired webhook-idis theevt_id, derived for every type from the event type and its object, so every re-emission deduplicates.deposit.pendinganddeposit.are no longer sent: the quote'sconfirmed paymentshows a transfer before finality. The admin outbox replay and deposit view take and showevt_ids. -
Breaking: quotes replace rate locks (docs/
architecture. md §9, §12). POST /v1/quotes {account_id, amount, currency, chain_id, asset},GET /v1/quotes/{id}, andPOST /v1/replacequotes/ {id}/ cancel …/accounts/{ext}/rate-locks[/{ref}]; the quote id (qt_…) replacesproduct_lock_ref,Idempotency-Keymakes creation safe to retry, amounts are integer cents withcurrency: "usd", timestamps are Unix seconds, and statuses areopen,complete,expired, andcanceled. A new quote's address salt uses its id as the reference. Quoting by token amount is removed.GET /v1/configlists the payable assets, limits, and quote terms;POST …/accountsandGET …/are removed (the first quote or address creates the account).accounts/ {ext}/ limits -
Breaking: quotes are the only flow.
GET|POST …/accounts/{ext}/deposit-address,…/deposit-address/rotate,GET …/, and theaccounts/ {ext}/ pending- deposits addressespause scope are removed. -
Breaking: deposits and refunds are top-level resources.
GET /v1/deposits(a Stripe list object withstarting_after/ending_before/limitand filtersaccount_id,quote,status,tx_hash,created[gte|lte]) andGET /v1/returndeposits/ {id} Depositobjects (dep_ids,status,amountin cents,exchange_rate,price_sourcequoteorspot,quote,amount_,refunded_ atomic refunded, Unix timestamps);POST /v1/refunds {deposit, destination_withaddress, amount_atomic?} Idempotency-KeyandGET /v1/refunds/{id}returnRefundobjects (re_ids, statuspendingorsucceeded).expand[]expands a deposit'squote, a quote'sdeposit, and a refund'sdeposit. The old deposit list, deposit, support lookup, and refund-request paths are removed; the operator'sGET /v1/shows a deposit's transitions and webhook events, and account pause and resume move toadmin/ deposits/ {id} POST /v1/.admin/ products/ {slug}/ accounts/ {account_ id}/ pause| resume -
POST /v1/quotesreturns aclient_secret, like Stripe's PaymentIntent. The payer's browser reads the quote's public view,ClientQuote, fromGET /v1/without a signature (any origin; rate-limited). Only the secret's hash is stored:quotes/ {id}? client_ secret= … GETreturnsnull, and a repeat with the sameIdempotency-Keyreturns a new secret. -
Breaking: the product is identified by the request signature's key id,
{product}/v1, not by the path. -
Breaking: errors are Stripe's error object,
{"error": {"type", "code", "message", "param"}}, with Stripe-style codes (parameter_,invalid resource_missing,signature_,invalid rate_limit,idempotency_, …).key_ reused pausedandchain_frozenanswer409instead of423. -
Route files name only what differs per route or environment (
route,version,product,chain.{chain_id, forwarder_,factory, treasury} asset.{symbol, contract, decimals},pricing., and{primary, check} limits); every other value is a code default, overridable under its key (architecture §14).topup route show FILEprints the resolved route. The implementation defaults to the factory's firstCREATE, the sanctions oracle to Chainalysis's address on chains that have one, and a product's key id is{product}/v1.finality,destination., andproduct_ kid rate_are removed; thelock. enabled quotespause scope stops quote creation. Staging's route moves to version 2. -
A new quote's
amount_atomic(and itspayment_uri) is rounded up to the route'squote.token decimals, default 4, so the payer is asked foramount_ decimals 273.9185PHA rather than 18 decimals. The rounding overpays by less than one unit of the last decimal; the quote'samountcredit is unchanged. -
The admin deposit
nudgeand refundapprove/recordpaths take thedep_andre_ids the product API returns, as the admin deposit view and outbox replay already did. Their responses (Admin,Refund Response. id Nudge, and the deposit view'sResponse. deposit_ id id) show the prefixed id instead of the UUID. -
A malformed path parameter, query string, or JSON body on any route, product or admin, answers the
400error object (parameter_,invalid parameter_, ormissing parameter_, withunknown param) instead of plain text; the admin routes' JSON bodies were plain text before. -
Breaking: webhook fulfillment replaces the settlement protocol (architecture §7, §11; integration guide §5). A deposit that passes screening is
crediteddirectly (confirmed → credited; theclearedstate is gone), anddeposit.creditedis the fulfillment event: the product creditsamount_minortoexternal_idonce per deposit id and answers2xx. Its payload is nowproduct_id,external_id,deposit_id,state,unit,amount_minor,price_source,price_scaled,price_scale,valuation_at,product_lock_ref(the receiving address's lock, also for spot-priced payments),address,route,route_version,chain_id,asset_contract,tx_hash,log_index, andamount_atomic(nodestination_), and itstx_ id webhook-idisuuid_, the same on every delivery and after a restore. Deliveries retry untilv5(DEPOSIT_ NAMESPACE, "deposit. credited: <deposit_ id>") 2xx. The service no longer sendsPOST {settlement_url}orGET {settlement_, and no deposit becomesurl}/ {key} rejected(product_any more: a product refuses a credit by holding it and requesting a refund. Allowed insiderefused) /v1without a deprecation window because no product consumed the settlement protocol in production (owner decision on #143). -
The attested route's
destination.is removed (routes that set it no longer load). A product'ssettlement_ url webhook_urlmay behttponly when the service's own public origin is. -
GET /v1/route entries replaceadmin/ report/ daily settlements_withby_ status credited_andundelivered credited_:undelivered_ max_ age_ seconds deposit.creditedevents the product has not acknowledged yet (administrative API). -
POST /v1/acceptsproducts/ {p}/ deposits/ {id}/refund-requests creditedandsweptdeposits too (still notsanctioned, still at least the route'smin_): a product asks to refund a credit it did not apply or has reversed, for example for a closed workspace; finance approves every request (integration guide §5.4).refund_ atomic -
Settlement conformance: the
unknown_getcase now requires404forGETof an unknown settlement key and fails200 {"status":, which it used to accept. The service resends a settlement only after a"unknown"} 404by key; the other answer made it poll without ever resending.topup-answers the old form and fails exactly that case.conformance- reference --broken unknown-status -
Breaking (base URL): the service is served on a custom domain,
https://(stagingcrypto- topup- api. phala. com https://), with TLS terminated inside the CVM and its certificate evidence atcrypto- topup- api- staging. phala. com /evidences/. Sign@target-urifor that origin; the gateway URLhttps://no longer answers.<app_ id>- 8080. <gateway domain> -
Removed the unreachable
501response from/v1/attestationand thework_packageerror field fromopenapi.json; both belonged only to the pre-C11 placeholder, and production never returned them (#90). -
Rate locks carry an optional
paymentobject, chosen by the lock consumption rule: the deposit that consumed the lock, otherwise the first payment that would consume it, otherwise the first payment.statusis"seen"while it is abovefinalized(withconfirmationsandestimated_, block time plus 15 minutes) andfinal_ at "finalized"once it is a deposit (deposit_idlocates it);supported,in_time, andamount_describe it against the lock and are false on a cancelled lock.within_ tolerance -
New
GET /v1/lists transfers to the account's persistent addresses seen aboveproducts/ {p}/ accounts/ {ext}/ pending- deposits finalized. They are not deposits and are not credited; once final they leave this list and appear underdeposits, and a reorg can remove them.
See docs/ §8 and §12. Both are display only: crediting is unchanged and still
happens only from two-provider finalized data.
-
New event
deposit.pending, sent at most once per chain event when a non-zero transfer of a routed token to a watched address is first seen abovefinalized. Its payload is markedprovisional: true; it never changes a balance, and the transfer may still disappear in a reorg. It may arrive afterdeposit.creditedfor the same deposit, so act on fetched state, not event order. Receivers that do not handle it must ignore it, as with any unknown event type. -
deposit.creditedanddeposit.rejectedpayloads now includechain_id,state(creditedorrejected), androute(null when no route was selected);chain_idandroutematch the fields already ondeposit.. This covers every producer, including the scanner'sconfirmed unsupported_rejection. The change is additive; existing fields are unchanged. Seeasset docs/§12.architecture. md -
A lock now expires by chain time:
rate_is emitted only once the finalized chain has passedlock. expired expires_atand no payment mined inside the window awaits confirmation, so a payment made in the last minutes of the window is consumed at the lock price and never reported as expired. Until thenGET …/rate-locks/{ref}returnsstatus: openwithremaining_; expiry events arrive about 15 minutes afterseconds: 0 expires_at. Seedocs/§9.architecture. md -
DELETE …/rate-locks/{ref}on a lock whose payment window has closed but which has not yet expired now answers409with the new error codewindow_closed("payment window has closed") instead ofconflict.conflictremains for consumed or expired locks. -
GET …/limitsreset_atis the earliest payment-window close among open reserved locks. It can be in the past: exposure is released only at chain finality, about 15 minutes later.
Removed
-
The Release images workflow and the fork requirement: nobody needs a fork or their own image build to deploy (building from source stays possible and yields the same
phala-paydigest). -
The reference product's
team_addressestable, written with each quote and deposit address but never read: the quote and deposit address records hold them. -
The reference product's ledger migrations: it creates its one current schema in a new ledger and reads no older one, so an existing ledger is reset (Phala's staging's is).
-
Webhook events written before Stripe-style events (outbox format 1) and their old envelope; every event is
{id: "evt_…", object: "event", type, created, data: {object}}. -
The retired
rejected(product_reason andrefused) clearedstate, and addresses issued before quotes (persistent addresses). -
Settlement conformance suite (
topup-,conformance topup-,conformance- reference docs/,conformance. md make product-) and the reference product's conformance mode (test accounts and theconformance _conformance/hook). The settlement endpoint it tested is being replaced by webhook fulfillment (integration guide §5); webhook receivers are tested withledger topup-sdk send-test-event. -
Breaking (administrative API):
GET /v1/route entries no longer carryadmin/ report/ daily exposure_minor,exposure_,minor_ reason pnl_minor, orpnl_minor_reason(#94). They were always null placeholders; route exposure now comes from the report-levelexposure_minor, and PnL is not defined precisely enough in the design to compute.
Fixed
-
A
client_secret's 120 reads per minute of its quote or deposit address refill at two a second instead of all at once when a one-minute window rolls over, so a page can no longer read 120 times just before a rollover and 120 more just after it. A read over the budget is retryable afterRetry-After: 1rather than at the end of the window. -
Admin:
POST /v1/refuses,admin/ restore/ deposit_ addresses 400, aversionmore than 32 past the customer's latest one, before anything is issued: it issued every version between in one transaction, however many, so a hugeversionheld its connection and locks until it exhausted them. A customer further behind is re-issued in steps (version32, 64, …); anaddresswas already looked for only that far. -
Admin: a restore across a treasury change no longer deadlocks the reconciliation. Deposit addresses and quotes are re-issued over a treasury in force when they were issued, within 5 minutes (any since the restore point for a deposit address, around its
createdfor a quote), not only the current one, once the lost change is applied again withPOST /v1/; before, they could be re-issued only over the restored current treasury, while the change would apply only after the unfreeze those re-issues must precede. Each re-issued deposit address version, by address or by version, keeps a superseded network over every treasury in force since the restore point, still credited. A restore without a restore point re-issues nothing, and a quote the restored database does not hold, created well before the restore point, is refused (one it holds is returned,admin/ restore/ treasuries/ apply reissued: false). -
A quote's
createdis taken once its chain's treasury is read under the treasury lock, and a time-locked treasury change recordsapplied_atas it applies under that lock rather than when the time-lock's pass started, so each falls while the other's treasury is in force. -
Admin: a deposit reversed after the restore point because a re-included transaction put another transfer at its receipt position keeps its identity through the restore. Its imported
deposit.reversedrebuilds it, reversed, at its revision, so the rescan records the final transfer as its successor again, with the same id,replaces, and delivered credit; before, the rescan recorded it under the reversed deposit's id, held ascontradicted, and the successor could never be rebuilt. -
Admin:
POST /v1/imports theadmin/ restore/ events deposit.reversedof a deposit that was never valued (rejected, such as a token without a route); it refused the whole request with400. Adeposit.creditedstill needs its valuation. -
Idempotent requests are atomic (architecture §12; Brandur Leach's Stripe-like idempotency keys in Postgres): every merchant
POSTsaves its response in the transaction of its changes, so a retry after a crash, a dropped connection, or a request slower than a minute replays the result and never runs the request twice (a quote, a partial refund, an API key, a webhook endpoint, or a webhook key roll was created twice before). A key whose request never saved a response is still taken over by the same request after a minute, and the request it replaced can no longer commit: it answers409 idempotency_. A request already making its changes commits, and the repeat waits for it (at most 5 seconds, thenkey_ in_ use 409 idempotency_) and replays its response. A failure while rendering a response now creates nothing and is replayed as it failed (a quote was created before). A request the database cannot begin, or rolls back on a deadlock or serialization failure, is an unsavedkey_ in_ use 503withRetry-After, as is one that finds no database connection to claim its key (it was a500). -
Admin:
POST /v1/andadmin/ restore/ quotes /deposit_accept aaddresses client_secretonly when the service issued it for that id to that account. The secret's nonce now carries an owner tag of the account (its length and format are unchanged, and it is still opaque): before, any account's record of a lost id with the owner's secret, which the payer's page holds, re-issued it under that account and address, and the owner's payer page showed that address. -
Authorization runs before the idempotency lookup, as Stripe's: a restricted key no longer replays a response to a request its permissions refuse, and a
401or403is no longer saved, so the same request by a key that holds the permission then runs. -
A payment made through a contract (a router, a swap output) whose transaction is re-included before finality against other state, so that the transfer at the same receipt position pays another amount or another issued address, is now recorded and credited. The first deposit is
reversed(deposit.reversedif you were told of it), and the transfer in the final chain is a new deposit with a new id and its owndeposit.credited(ordeposit.rejected), already final, whosereplacesnames the first one; a quote the first one completed goes to it withoutquote.expired. The two deposits' events arrive in no set order: the balance rule nets them whatever the order. The new transfer was never recorded, and custody reconciliation froze the chain. Existing deposit ids are unchanged, and a transaction re-included unchanged keeps its deposit. -
After a restore from backup, a deposit the merchant was told was credited keeps that credit: the amount, exchange rate, price source, and valuation time of the imported
deposit.creditedordeposit.reversedare the deposit's valuation when the rescan re-derives it, instead of a re-valuation at spot, soamount,amount_refunded, andamount_reversedmatch what was delivered. A deposit whose transfer on chain contradicts the delivered event is held, not credited, until the operator discards the delivered credit. -
A token without a route sent to an issued address is again recorded as
rejected(unsupported_, with itsasset) deposit.rejectedevent, once final. Since the per-block scanning change, routes in token mode (the default) never saw such transfers: the missing-deposit check now reads every issued address's transfers of any token in both modes. -
A webhook endpoint's
pending_anddeliveries oldest_, and the admin daily report'spending_ at failing_, no longer count the notice of a URL change still pending at the endpoint's former URL: it is not a delivery to the endpoint as it is now, so a former URL that was taken down no longer makes the endpoint look unhealthy. The notice is still retried until delivered.webhook_ endpoints -
Webhook deliveries no longer stall behind a URL change's notice. The notice goes to the endpoint's former URL, and its failures there counted as the endpoint's: once that URL was taken down, the endpoint cooled down and every probe picked the failing notice first, so new events were held for up to an hour at a time. A notice's outcome now neither cools nor clears the endpoint.