Work & reportsSample report

VeltoraSecurity Assessment Report

app.veltora.io

Date of engagement: 31 August 2026 - 11 September 2026

  • Piotr CielasPrincipal advisor
  • Timur GüvenkayaSpecialist
  • Łukasz MikułaSpecialist

Engagement Dashboard

Assessment conclusion

Anyone who knew the settlement webhook URL could credit any account with any amount and withdraw it as real funds. The route checked no signature. We reported it on day two, Veltora shipped signature verification and a replay guard two days later, and the fix is in the baseline this draft covers. Two High findings remain open. A payment link ID that turns up in a log, a support ticket or a screenshot recovers the escrow key and claims the funds. Any member of an organisation can change the recipients of a payout batch after an approver has signed it off. Four Medium findings need a precondition first: unauthenticated passkey writes that stall the credential backup, a session that survives sign-out on a shared browser, an owner signing path that forwards whatever typed data it is given, and a confirmation screen that shows only eight characters of the recipient. Before the next release: re-issue the payment link format, freeze approved payout batches on the server, clear the refresh cookie on sign-out, and allowlist what the owner path will sign. Relaymint settles and Wardkey holds the embedded keys. We did not review either service, only how this application handles what they return.

Engagement record

Client
Veltora Labs Ltd.
System
Veltora - app.veltora.io: the Next.js web wallet for passkey sign-in, embedded wallets, send, swap, payment links and recurring organisation payouts over the Relaymint settlement API and Wardkey embedded wallets
Reviewed baseline
a41c07e9d2b85f3e6c19d04b7a2e58f1c93d6b20
Reviewed scope
8 groups of files, read line by line or as supporting context. The Scope section lists them
Authored security artifacts
12 *.security.test.ts files, three working proofs of concept, and handover notes for the suite
Principal advisor
Piotr Cielas
Specialists
Timur Güvenkaya, Łukasz Mikuła

Timeline

  1. Review start31 August 2026
  2. Review end11 September 2026
  3. Draft report15 September 2026

Findings by severity

  • Critical 1
  • High 2
  • Medium 4
  • Low 3
  • Informational 4

Assessment snapshot

7Flows assessed
13Open findings
71Test cases added

Threat scenarios

19Assessed
12Issue identified
7No issue identified

Security invariants

13Evaluated
8Do not hold
1Partly hold
4Hold

Advisory and Client

The advisory

Guvenkaya specializes in security for digital assets and financial infrastructure, from smart contracts and protocols to applications, infrastructure, custody and cryptography.

Our team brings experience from Kraken, OpenZeppelin, Binance, ZKsync, Nillion, Invicti and EY, alongside the sitting Heads of Security at Gauntlet and Agora. Across the team: 200+ security reviews, $30B+ in volume secured, 13 published cryptography research papers, and credentials including OSCP, OSWE and CISM.

  • Every reviewer has a public bio and is named before you sign. We match expertise to your system, with a principal leading scoping and threat modeling, and checking findings.
  • Alongside findings, you receive a threat model, invariant register, new tests in your repo and a record of the attack scenarios we tested.

The client

Veltora is a web wallet for individuals and small businesses. Users sign in with a passkey or an external wallet, hold funds in an embedded wallet custodied by Wardkey, send and swap across chains through the Relaymint settlement API, and share claimable payment links. Organisations run recurring payouts to contractors from a shared balance, with an approver role that signs off each batch.

The application under review is the Next.js frontend and its API routes. It constructs, displays and signs every payment those flows execute. Settlement belongs to Relaymint and key custody to Wardkey, and both sit outside this repository as trust boundaries.

  • The system we reviewed builds, signs and submits. It does not settle.
  • Keys live with Wardkey; quotes and settlement live with Relaymint.
  • The server-side controls live in three places: the API routes, one service-role database client, and a scheduled payout job.

System Components and Trust Boundaries

The diagram separates the code we read from external services, data stores, privileged jobs and inherited libraries. Each component carries the number of findings that landed on it, by severity, so you can see which parts carry the most, and how severe, before reading a single finding.

What a boundary means

Reviewed scope
Veltora-controlled Next.js routes, state machines, the payout job and client construction of quotes, payments and signatures.
Trust boundary
We took the documented behaviour on trust and reviewed how Veltora copes when it turns out to be wrong. Relaymint, Wardkey and the chain RPC providers.
Inherited
We reviewed the parts the application actually calls. If the library changes, that is a new review.
Capability channel
A secret that belongs in a URL fragment, a cookie or a local signer and nowhere else. If it reaches a log, an API response or another user, confidentiality is gone.

The database client always holds the service-role key, so the application code is the only authorization there is. Everything else in this report serves one requirement: the asset, amount, recipient, fee and signer shown on screen have to be the ones the application authorizes and submits.

Input ownership

This table records who gets to set each value, which is what decides whether a bad value is a finding. An operator account someone has stolen is not in this table; that is outside the attacker model.

OwnerWhat they control
AttackerPaths and IDs; query strings; bodies; headers on non-browser clients; wallet addresses, recipients, amounts, tokens and slippage; payment link blobs; passkey credential IDs and public keys; webhook bodies sent to a public URL; every response from Relaymint, Wardkey, RPC providers and token metadata
Organisation memberPayout batch drafts, CSV imports and recipient lists within their own organisation
OperatorEnvironment variables and secrets; webhook signing keys; fee settings; feature flags; database schema; job schedules
DeveloperSource, lockfiles, framework configuration, static content and scripts

Privileged authorities · External entity

Operators and admin console / Payout scheduler (JOB_SECRET)

SER1 I

External entity

Passkeys / authenticators

WebAuthn ceremonies; owner key for the embedded wallet

ST1 M

External entity

Browser / same-origin scripts

Any script on this origin can reach the owner signing path

TIE1 M

External entity

Organisation members

Draft, import and edit payout batches

TE1 H1 L

Process

app.veltora.io Next.js application / reviewed scope

STRIDE
  • Session and token1 M
  • Passkey credentials1 M
  • Embedded wallet and signing1 M1 I
  • Balances, send and withdrawal1 C1 M1 I
  • Swap and quotes1 L1 I
  • Payment links1 H1 L
  • Recurring payouts1 H1 L1 I

External process

Relaymint API

Quotes, settlement and signed callbacks

ST1 C1 L

External process

Wardkey

Embedded wallet custody, signing and export

TEI1 M

External process

Chain RPC providers

Balances, nonces and broadcast

T

Data store

Postgres (service role)

All application tables. The app is the authorization.

TID2 H1 M1 L

Data store

Browser storage

Cookies and the IndexedDB session signer

IT1 M1 I

Data store

Logs / CDN access logs

Logged link IDs decrypt permanently

I1 H

Data store

Backup object store

Nightly passkey credential snapshot

ID1 M

Inherited and vendored surface

Inherited component

jose JWT verification

Local JWKS; audience and issuer enforced

S

Inherited component

relaymint-sdk

Quote and callback client

T
  • Reviewed scope
  • Trust boundary, behaviour assumed
  • Inherited or vendored code
  • Findings that landed on that component, counted by severity
  • Letters are the STRIDE classes prioritized for that element in this engagement: S spoofing, T tampering, R repudiation, I information disclosure, D denial of service, E elevation of privilege

Security-Critical Flows

We worked through seven flows: identity and signing authority first, then money movement in the order the developers ranked it, then payouts.

01Authentication and session

Invariants
INV-01, INV-02
Findings
1 MGUV-5

Path through the system

  1. Wallet or passkey
  2. Challenge
  3. Session token
  4. Cookies
  5. Route

Security properties

Who you are comes from a verified session token, checked against the wallet the client claims. A path or body parameter cannot pick a different account. Signing out ends the session.

02Passkey registration and recovery

Invariants
INV-03, INV-12
Findings
1 MGUV-4

Path through the system

  1. Authenticator
  2. POST /api/passkeys/credentials
  3. Postgres
  4. Backup

Security properties

A public key is stored only after a real registration ceremony. Unauthenticated callers cannot grow the credential table until the backup fails.

03Embedded wallet and signing authority

Invariants
INV-04, INV-05, INV-02, INV-13
Findings
1 M1 IGUV-7GUV-13

Path through the system

  1. Session
  2. Wallet lookup
  3. Passkey proof
  4. Wardkey
  5. Session signer

Security properties

A caller reaches only their own embedded wallet. The owner path signs only what the user has seen decoded. A session signer does not outlive the wallet it was issued for.

04Balances, send and withdrawal

Invariants
INV-06, INV-07, INV-11
Findings
1 C1 M1 IGUV-2GUV-8GUV-11

Path through the system

  1. Relaymint callback
  2. Ledger
  3. Review
  4. sendMachine
  5. Refund

Security properties

A ledger credit comes only from a signed, fresh settlement event, once. What the user reviews is what is sent, and the review shows enough of the recipient to tell a lookalike apart. A failed withdrawal refunds once, to the source.

05Swap and quote execution

Invariants
INV-06, INV-08
Findings
1 L1 IGUV-6GUV-14

Path through the system

  1. Form
  2. Dry quote
  3. Review
  4. swapSubmitMachine
  5. Live quote

Security properties

The reviewed quote is a dry run and submit fetches a live one, so the two have to be compared before anything is signed.

06Payment links

Invariants
INV-09
Findings
1 H1 LGUV-1GUV-9

Path through the system

  1. Escrow keypair
  2. Encrypt
  3. Store
  4. Fragment link
  5. Claim

Security properties

The link ID on its own cannot decrypt the link or recover the escrow key. The complete link is a bearer instrument by design.

07Recurring payouts

Invariants
INV-10, INV-11
Findings
1 H1 L1 IGUV-3GUV-10GUV-12

Path through the system

  1. Draft batch
  2. Import
  3. Approve
  4. Scheduler
  5. Relaymint

Security properties

Only an approver can release a batch, and a batch cannot change after approval. The job that submits it takes a lease and never submits the same batch twice.

Scope and Review Boundaries

Review depths

Deep
Line-by-line review with explicit attack hypotheses, and a reproduction attempt against the staging deployment for each hypothesis that survived reading.
Supporting
The code was read for its effect on a reviewed flow. Behaviour outside that effect was not assessed.
Targeted
Entry points were exercised or triaged against the flow's security properties rather than read in full.
Trust boundary
We took the documented behaviour on trust and reviewed how the application copes when it turns out to be wrong. We did not review the component itself.

This is the code we read, and how closely.

PathReview depthPrimary flow
src/features/auth/, src/lib/auth/verifySessionToken.tsDeepAuthentication and session
src/middleware.ts, src/app/api/activity/SupportingAuthentication and session
src/app/api/passkeys/, src/app/api/jobs/backup-passkeys/route.tsDeepPasskey registration and recovery
src/app/api/wallets/, src/features/signing/, localSessionSigner.tsDeepEmbedded wallet and signing
src/app/api/webhooks/settlement/route.ts, src/features/send/, src/features/withdraw/machines/DeepBalances, send and withdrawal
src/features/swap/, src/lib/relaymint/DeepSwap and quote execution
src/app/api/links/, link encoder and claim flowDeepPayment links
src/app/api/payouts/, src/jobs/payoutScheduler.tsDeepRecurring payouts
src/app/api/payouts/import/, CSV parserTargetedRecurring payouts
Relaymint, Wardkey, chain RPC providers and hosting platform internalsTrust boundaryall
Presentational components, marketing pages, analytics dashboards, unrelated migrationsExcluded

Out of scope

We did not run destructive tests against production, load-test for denial of service, attempt social engineering, or touch accounts and funds that were not ours.

Assessment Framework

This section defines the words the Findings Summary uses. Every finding in this report records an impact and a likelihood, and its severity is what the matrix below returns for that pair. Nothing else sets it.

Every finding goes through the same chain, from the attack scenario we modelled to what we are asking you to do about it.

Risk chain applied to every finding

  1. 01 Threat scenario TM-xx, from the threat scenario register
  2. 02 The weakness What we actually found in the code
  3. 03 Likelihood Likely, possible or rare
  4. 04 Impact Severe, high, moderate or negligible
  5. 05 Severity Derived from the matrix below
  6. 06 What to do Fix it, accept it with a compensating control, or watch it

Impact

What happens if someone succeeds, judged against this system and this client.

ImpactWhat it means here
SevereUser or organisation funds are lost or taken at scale, or the signing authority behind them is compromised, with no precondition the attacker does not already control.
HighFunds or an account are lost, taken over or exposed, but the attacker needs a precondition first: a leaked identifier, organisation membership, physical access to a signed-in browser, or an upstream service behaving dishonestly.
ModerateA security control is lost, bypassed or degraded with no realised loss behind it, so harm needs a second and independent failure. Also private data reaching someone who should not see it, and availability lost on a path that has a manual recovery.
NegligibleThe behaviour is wrong, or a defence is missing, and we could demonstrate no security consequence.

Likelihood

How easy the problem is to find combined with how hard it is to exploit. It is not a forecast of how often it will happen.

LikelihoodWhat it means here
LikelyReachable by an unauthenticated caller with no special knowledge, with the effect following from a normal request.
PossibleReachable, but the attacker has to do some work: hold an identifier, win a race, accumulate state, or already be authenticated.
RareNeeds a precondition the attacker does not ordinarily get: an upstream API or platform behaving dishonestly, or physical access to a signed-in device.

Severity

Severity comes out of the matrix mechanically, from the impact and likelihood we recorded. Correctness and hardening findings carry Informational, and we do not count them as vulnerabilities.

Likelihood / ImpactSevereHighModerateNegligible
LikelyCriticalHighHighMedium
PossibleHighHighMediumLow
RareMediumMediumLowInformational

Remediation complexity

Every finding carries an effort estimate as well. Simple is a local change with no consequences for stored state. Moderate means the fix touches the data model, a migration, caching behaviour or a shared component. Involved means the design has to change.

Annex D records the external frameworks this method aligns with.

Findings Summary

The one Critical finding is fixed. Both High findings are open. There are 10 security findings in total: one Critical, two High, four Medium and three Low. A further four findings are correctness or hardening work, rated Informational; they carry GUV numbers so engineers can track them, and they are not counted as vulnerabilities.

Counting findings is not the same as counting causes. GUV-1 and GUV-9 are one link-format problem written up twice because they need separate fixes, and GUV-3 and GUV-10 are two gaps in the same approval control.

14All findings
1 Critical

Unsigned settlement webhook credited balances. Fixed during the review

2 High

A link ID that claims the link, and payout batches editable after approval

4 Medium

Passkey table flooding, a session that survives sign-out, an unconstrained owner path and a truncated recipient

3 Low

2 demonstrated defects and 1 that depends on an unverified external guarantee

4 Informational

3 correctness bugs and 1 piece of hardening

Finding types

Defect
We showed an attacker can cause the effect in the code we reviewed.
Conditional
The effect needs an external service or state to behave in a way we could not check.
Correctness
The code gets state, arithmetic or display wrong, with no security consequence we could demonstrate.
Hardening
A defence worth adding. We found no way to exploit its absence.

Severity is not a judgement call. Every finding records an impact and a likelihood, and the severity in the table below is what the matrix returns for that pair. The Assessment Framework above defines all three scales and the Simple / Moderate / Involved scale in the remediation-complexity column.

Findings register

FindingTypeImpactLikelihoodSeverityRemediation complexityStatus
GUV-1: Payment link ID alone recovers the escrow key and can claim a funded linkDefectHighPossibleHighModerateOpen
GUV-2: Settlement webhook accepted without signature verification, crediting ledger balancesDefectSevereLikelyCriticalSimpleFixed
GUV-3: Any organisation member can change payout recipients after approvalDefectHighPossibleHighModerateOpen
GUV-4: Unauthenticated passkey writes grow the credential table and stall the backupDefectModeratePossibleMediumModerateOpen
GUV-5: Sign-out leaves the refresh cookie behind, so the session resumes without a passkeyDefectHighRareMediumSimpleOpen
GUV-6: Submit signs a live quote without comparing it to the reviewed quoteConditionalModerateRareLowModerateOpen
GUV-7: Owner signing path forwards arbitrary typed data with no server-side allowlistDefectModeratePossibleMediumInvolvedOpen
GUV-8: Confirmation screen shows only eight characters of the recipient addressDefectModeratePossibleMediumSimpleOpen
GUV-9: Payment link rows never expire and stay decryptable after claimDefectModerateRareLowModerateOpen
GUV-10: Payout batch total computed in the browser and stored without recomputationDefectNegligiblePossibleLowSimpleOpen
GUV-11: Displayed network fee uses a cached rate; the submitted fee uses a live oneCorrectnessNegligibleRareInformationalSimpleOpen
GUV-12: Payout scheduler takes no lease, so overlapping runs resubmit a batchCorrectnessNegligibleRareInformationalModerateOpen
GUV-13: Session signer key survives a wallet switchHardeningNegligibleRareInformationalSimpleOpen
GUV-14: Price impact computed against output rather than inputCorrectnessNegligibleRareInformationalSimpleOpen

Detailed Findings

Each finding says what we actually demonstrated, then says separately what would have to be true for the worst case, so you can tell the two apart. Then the code, then the change that closes it. Correctness and Hardening findings use the same Security impact heading; there it explains why the item matters operationally, and does not claim an exploit.

GUV-1: Payment link ID alone recovers the escrow key and can claim a funded link

Severity
High
Type
Defect
Threat scenario
TM-01
Invariants
INV-09
Status
Open
Impact
High
Likelihood
Possible

Security impact

A payment link is meant to be a bearer instrument: whoever holds the complete link, including the fragment after #, can claim the funds, and nobody else can. The fragment carries the IV that the encrypted escrow key needs. The link ID in the path is supposed to be useless on its own, which is why it is safe to log, and it does get logged: by the CDN, the hosting platform, the error monitor and every support ticket that pastes a failing link.

The ID is not useless. GET /api/links/[linkId] returns the AES key with the ciphertext and the GCM tag, and with the key in hand the IV is not secret. XOR the tag with the GHASH of the ciphertext to get the encrypted counter block, decrypt that block with the key, and the first 96 bits are the IV. The key and the IV then decrypt the escrow key. Our proof of concept recovered the escrow private key from a link ID alone and signed a claim with it, using the application's own encoder, without claiming a live link.

So anyone who can read a log line that contains a link ID can claim that link while it is funded. The attacker needs the ID, which is why likelihood is Possible and not Likely.

src/app/api/links/[linkId]/route.ts
export async function GET(_req: Request, { params }: { params: { linkId: string } }) {  const { data } = await db    .from("payment_links")    .select("ciphertext, aes_key, expires_at")    .eq("id", params.linkId)    .single()  return Response.json({ ciphertext: data.ciphertext, key: data.aes_key })}

Recommendation

Stop returning the key. Split the secret so that the server holds nothing that decrypts the link: derive the encryption key from material that lives only in the fragment, and make the link ID a random value with no relationship to it. Re-issue every unclaimed link under the new format and invalidate the old ones.

Remediation and verification

Open

GUV-2: Settlement webhook accepted without signature verification, crediting ledger balances

Severity
Critical
Type
Defect
Threat scenario
TM-02
Invariants
INV-11
Status
Fixed
Impact
Severe
Likelihood
Likely
Fix commit
9c3e1a7f04b2d86e5a1f7c20e4d93b58a6f2c071
Verification
Code review of the fix commit plus two contributed regression tests

Security impact

POST /api/webhooks/settlement is where Relaymint tells Veltora that a deposit or swap has settled. The route parsed the body, looked up the user by the account field and credited the ledger with amount. It did not check the Relaymint-Signature header, did not check a timestamp, and did not record the event ID. The URL is in the public client bundle because the staging build also uses it for polling.

Anyone could therefore credit any account with any amount, and the ledger balance is what the withdrawal flow spends from. We credited a test account on staging with a forged event and withdrew the credit to an external address. This needed no account, no identifier and no race, which is what Likely means, and the impact is funds taken at scale.

We reported this on the second day of the review. Veltora shipped signature verification and an event-ID uniqueness constraint two days later. The fix commit is in the baseline this draft covers, and we verified it there.

src/app/api/webhooks/settlement/route.ts
export async function POST(request: Request) {  const event = settlementSchema.parse(await request.json())  const account = await findAccount(event.account)  await ledger.credit(account.id, event.amount, event.asset)  return new Response(null, { status: 204 })}

Recommendation

Verify the Relaymint signature over the raw body before parsing, reject events older than five minutes, and record each event ID under a unique constraint so a replay credits nothing. Treat the credit as the last step of a transaction that includes the event-ID insert.

Remediation and verification

Fixed in commit 9c3e1a7f04b2d86e5a1f7c20e4d93b58a6f2c071, which is included in the reviewed baseline. The route now verifies the signature over the raw body with a constant-time comparison, rejects stale timestamps, and inserts the event ID under a unique constraint in the same transaction as the credit. Two contributed regression tests cover a forged signature and a replayed event; both pass.

GUV-3: Any organisation member can change payout recipients after approval

Severity
High
Type
Defect
Threat scenario
TM-03
Invariants
INV-10
Status
Open
Impact
High
Likelihood
Possible

Security impact

A payout batch has a draft state, an approved state and a submitted state. The approver role exists so that one person cannot both prepare and release a payment. The UI hides the edit button once a batch is approved and shows it only to editors, but PATCH /api/payouts/[batchId] checks only that the caller is a member of the batch's organisation. It does not check the caller's role, and it does not check the batch status.

So a viewer, or an editor after approval, can replace the recipient address on any line of an approved batch. The scheduler submits whatever the row holds at run time. We changed a recipient on an approved staging batch as a viewer and watched the scheduler pay the new address.

The attacker has to be a member of the organisation, which is why likelihood is Possible. The approval step exists because membership alone should not be enough to move money.

src/app/api/payouts/[batchId]/route.ts
export async function PATCH(request: Request, { params }: { params: { batchId: string } }) {  const session = await requireSession()  await requireOrgMember(session, params.batchId)  const patch = batchPatchSchema.parse(await request.json())  await db.from("payout_lines").upsert(patch.lines)  return Response.json({ ok: true })}

Recommendation

Enforce the role and the status on the server: only editors can change a draft, nobody can change an approved batch, and approval signs a hash of the lines that the scheduler re-checks before submitting. Record who changed what on each line.

Remediation and verification

Open

GUV-4: Unauthenticated passkey writes grow the credential table and stall the backup

Severity
Medium
Type
Defect
Threat scenario
TM-04
Invariants
INV-03, INV-12
Status
Open
Impact
Moderate
Likelihood
Possible

Security impact

POST /api/passkeys/credentials stores a credential ID and public key before any registration ceremony completes, and it asks for no session. The public key field has no length limit in the schema or the database. The nightly backup reads the whole table into memory and serializes it as one JSON value.

Anyone can therefore write rows until the backup job runs out of memory or time. Nobody is locked out when that happens. What is lost is the ability to restore passkey credentials if the live table is later lost, and account access depends on them. Rows never expire, so the condition persists until an operator clears it by hand.

We rate the impact Moderate because the harm is the loss of a recovery control rather than a realised loss.

Recommendation

Persist a credential only after a verified registration ceremony. Cap the public key length in the schema and the database. Stream the backup in bounded chunks.

Remediation and verification

Open

GUV-5: Sign-out leaves the refresh cookie behind, so the session resumes without a passkey

Severity
Medium
Type
Defect
Threat scenario
TM-05
Invariants
INV-02
Status
Open
Impact
High
Likelihood
Rare

Security impact

Sign-out clears the access cookie and leaves the per-wallet refresh cookie, which lives for fourteen days. On the next connect with the same wallet, the client finds no access token, exchanges the refresh cookie for a new one, and lands in a signed-in session. Nothing on that path asks for a passkey assertion.

So on a shared laptop with the wallet extension still unlocked, whoever connects that wallet next resumes the session without the passkey that is supposed to guard the embedded wallet and the payout routes. The cookie is httpOnly and SameSite=Lax, so it cannot be driven from another origin. That is why likelihood is Rare: it needs the device.

Recommendation

Delete the refresh cookie on explicit sign-out, and require a fresh assertion before a refresh restores a session that was signed out.

Remediation and verification

Open

GUV-6: Submit signs a live quote without comparing it to the reviewed quote

Severity
Low
Type
Conditional
Threat scenario
TM-06
Invariants
INV-06, INV-08
Status
Open
Impact
Moderate
Likelihood
Rare

Security impact

The quote on the review screen is a dry run. When the user confirms, swapSubmitMachine requests a live quote and signs the intent Relaymint returns. Nothing compares the live quote's recipient, output asset or minimum received with the reviewed one. If Relaymint returns different terms, through a bug or a compromise, the user signs terms they never saw. We rate it Rare because it needs Relaymint to misbehave.

Recommendation

Compare the live quote field by field against the reviewed quote and fail closed on any difference in recipient, asset, amount or deadline.

Remediation and verification

Open

GUV-7: Owner signing path forwards arbitrary typed data with no server-side allowlist

Severity
Medium
Type
Defect
Threat scenario
TM-07
Invariants
INV-05
Status
Open
Impact
Moderate
Likelihood
Possible

Security impact

POST /api/wallets/[walletId]/sign takes a typed-data payload and a passkey assertion and forwards both to Wardkey. It checks that the wallet belongs to the caller. It does not check what is being signed. The passkey prompt shows the same generic text for every request, so a user cannot tell a routine approval from a token allowance to an attacker.

Any script running on the page while the user is signed in can therefore request a signature over anything, and the user approves it believing it is routine. We rate likelihood Possible because it needs a script foothold on the origin, and impact Moderate because we demonstrated the missing control, not a drained wallet.

Recommendation

Allowlist the typed-data types the product actually uses, decode them on the server, and show the decoded action in the confirmation before the passkey prompt.

Remediation and verification

Open

GUV-8: Confirmation screen shows only eight characters of the recipient address

Severity
Medium
Type
Defect
Threat scenario
TM-08
Invariants
INV-06
Status
Open
Impact
Moderate
Likelihood
Possible

Security impact

The send confirmation renders the recipient as the first four and last four characters. Eight hex characters are 32 bits, so a matching address takes about four billion attempts, which is minutes on one GPU. Address poisoning then puts that address in the user's transaction history. The confirmation screen, which is the last control, cannot tell the two apart.

Recommendation

Show the full address on the confirmation, grouped for reading, and flag a recipient that is new to this user.

Remediation and verification

Open

GUV-9: Payment link rows never expire and stay decryptable after claim

Severity
Low
Type
Defect
Threat scenario
TM-01
Invariants
INV-09
Status
Open
Impact
Moderate
Likelihood
Rare

Security impact

Claimed and expired links keep their ciphertext and key in the table, and the GET route keeps serving them. With GUV-1 that means a link ID in an old log decrypts the link's contents indefinitely, including the sender's note and amount, after the funds have gone.

Recommendation

Delete the key when a link is claimed or expires, and return 410 for those links.

Remediation and verification

Open

GUV-10: Payout batch total computed in the browser and stored without recomputation

Severity
Low
Type
Defect
Threat scenario
TM-09
Invariants
INV-10
Status
Open
Impact
Negligible
Likelihood
Possible

Security impact

The CSV import sums the batch in the browser and posts the total with the lines. The server stores the total as sent. The approver screen leads with that total. An editor can make a batch look smaller than it is; the per-line amounts are still correct and still shown, which is why impact is Negligible.

Recommendation

Recompute the total on the server from the stored lines and ignore the client value.

Remediation and verification

Open

GUV-11: Displayed network fee uses a cached rate; the submitted fee uses a live one

Severity
Informational
Type
Correctness
Threat scenario
TM-12
Invariants
INV-06
Status
Open
Impact
Negligible
Likelihood
Rare

Security impact

The review screen shows a fee computed from a rate cached for up to sixty seconds. Submit fetches the live rate. In volatile periods the charged fee can exceed the shown one by a few percent.

Recommendation

Show the fee the submission will use, or cap the submitted fee at the displayed value.

Remediation and verification

Open

GUV-12: Payout scheduler takes no lease, so overlapping runs resubmit a batch

Severity
Informational
Type
Correctness
Threat scenario
TM-10
Invariants
INV-11
Status
Open
Impact
Negligible
Likelihood
Rare

Security impact

The scheduler picks up approved batches and marks them submitted only after Relaymint accepts them. Two overlapping runs both pick up the same batch. Relaymint rejects the second submission because Veltora sends the batch ID as an idempotency key, so nobody is paid twice. The job still does the work twice and logs a failure every time.

Recommendation

Take a row-level lease before submitting, and keep the idempotency key.

Remediation and verification

Open

GUV-13: Session signer key survives a wallet switch

Severity
Informational
Type
Hardening
Threat scenario
TM-11
Invariants
INV-02
Status
Open
Impact
Negligible
Likelihood
Rare

Security impact

The IndexedDB session signer is keyed by address and is not removed when the user switches wallet. Wardkey rejects its signatures for the new wallet, so we found no exploit. It is key material with no owner.

Recommendation

Delete the session signer on wallet switch and on sign-out.

Remediation and verification

Open

GUV-14: Price impact computed against output rather than input

Severity
Informational
Type
Correctness
Threat scenario
TM-12
Invariants
INV-06
Status
Open
Impact
Negligible
Likelihood
Rare

Security impact

The price-impact warning divides by the output value instead of the input value, so it understates impact on thin pools and the warning threshold triggers late.

Recommendation

Compute impact against the input value at the reference price.

Remediation and verification

Open

Remediation Plan

The findings above are the work. This section is how to sequence it: which findings are really one problem, what order to fix them in, what to do about the payment link IDs already in the wild, and which properties a remediation pass must not break.

Principal risk clusters

Three pairs of findings share one cause each. They stay as separate GUV numbers because engineers need separate fixes and separate tests, but each pair is one piece of work.

ClusterFindingsWhat it amounts to
The payment link format hands out its own keyGUV-1GUV-9One design mistake with two consequences: a link ID claims a funded link, and the link stays readable after it is spent.
Approval is a UI state, not a server ruleGUV-3GUV-10The server trusts membership where it should trust role and status, and trusts a client total where it should recompute.
A session that outlives its ownerGUV-5GUV-13Sign-out and wallet switch both leave credentials behind in the browser.

Remediation priorities

Fix the code in this order. Priorities 1 to 3 belong in the first response; 4 and 5 should land before the next release.

PriorityWhat has to be trueWhy it comes firstHow you would knowFindings
1The payment link format no longer lets the server hand out a decrypting key, and old links are re-issuedA link ID on its own claims a funded linkThe ID-only decryption test passes and the GET route returns no keyGUV-1GUV-9
2Approved payout batches are immutable on the server, and only editors change draftsAny member can redirect an approved payoutThe role and status negatives pass, and the scheduler re-checks the approval hashGUV-3GUV-10
3Signing out ends the session and removes every credential the browser holdsA shared browser resumes the previous user's sessionSign-out deletes the refresh cookie and the session signerGUV-5GUV-13
4The owner path signs only allowlisted, decoded actionsAny script on the page can get an arbitrary signature approvedPer-type schemas and a confirmation test that renders the decoded actionGUV-7
5What the user signs is what they reviewed: the full recipient and the reviewed quoteA lookalike address or a changed quote passes the last screenThe recipient and quote-mismatch tests pass at each signing call siteGUV-8GUV-6

Existing data and incident response

Fixing the code does not retire the payment link IDs already created. Anyone holding one from a log or a support transcript can still use it against the old format.

PriorityActionFindings
1Find unclaimed link IDs in CDN, platform, application, support and error-monitoring logs, and re-issue those links under the new format.GUV-1GUV-9
2Review payout line edits made after approval since the feature launched, and confirm each with the approver.GUV-3
3Reconcile ledger credits against signed Relaymint events for the period before the GUV-2 fix, and record the result.GUV-2
4Identify and remove unauthenticated passkey rows, and verify the most recent complete backup.GUV-4

What held

Three properties hold outright and one holds on the part that matters. Whoever does the remediation should not undo them, and each is pinned by a passing test in the suite we contributed.

PropertyStatePinned by
Wallet routes cannot read, sign with or export another user's embedded walletHoldsEVT-03EVH-02
A failed withdrawal refunds once, to the sourceHoldsEVT-07EVH-04
Session identity and cached responses stay with the callerHoldsEVT-10EVH-01EVH-06
A settlement event credits once, after the GUV-2 fixPartially holdsEVT-08EVH-07

Seven of the nineteen attack scenarios we modelled turned out not to work, and we have the evidence for why in each case. Annex A lists them.

Assurance Opinion

This draft records what we found as at 15 September 2026. The opinion below covers the baseline, scope and evidence named in this report and nothing else. It is not a statement that the application has no other defects. Change the reviewed code paths, the payout approval rules, the signing paths or the payment link format, and the opinion no longer covers the code you changed.

Security opinion

The one Critical finding was fixed during the review and the fix is in the baseline this report covers. We modelled 19 attack scenarios. Twelve produced a finding; seven closed with no issue found, each against recorded evidence. Of the 13 security properties we set out to test, 4 hold, 1 holds in part and 8 do not hold at this draft. We contributed 71 test cases across 12 files: 43 pass and 28 fail against this code, and the failing ones turn green as the fixes land.

Code reviewed
a41c07e9d2b85f3e6c19d04b7a2e58f1c93d6b20
Review period
31 August 2026 to 11 September 2026
Finding status
1 Fixed; 9 security findings and 4 correctness or hardening findings open
Attack scenarios
19 modelled: 12 found something, 7 did not
Scope of opinion
The seven security-critical flows
Signature of Piotr Cielas Piotr Cielas Principal advisor Guvenkaya Advisory, 15 September 2026

Annex A - Threat Model

We went through the system twice looking for threats. The first pass walks the components on the architecture map. The second walks the same system by its edges, asking what can go wrong between two parts rather than inside one. Both passes throw up candidates, and they are deduplicated into the single register at the end of this annex.

The six STRIDE classes

S
Spoofing. Pretending to be someone or something else.
T
Tampering. Changing data or code in transit or at rest.
R
Repudiation. Acting without leaving evidence of who did it.
I
Information disclosure. Reading what you should not be able to read.
D
Denial of service. Making something unavailable.
E
Elevation of privilege. Doing what your role does not permit.

The two passes

Element
Found by walking the components on the architecture map.
Interaction
Found by walking the edges between components, below.
Both
Both passes found it, independently of each other.

STRIDE is a prompt for the enumeration, not a complete list of what can go wrong with payments. Approval integrity, quote reminting and leaked capability links come from the invariants instead.

Actors

The architecture map shows the parts. This list shows who can set a scenario off. A compromised operator account and a poisoned dependency are both outside what we modelled.

01

Unauthenticated remote caller

Anyone on the internet, with no account. They reach the settlement webhook and the passkey routes.

Scenarios
TM-02, TM-04
Findings
GUV-2GUV-4
02

Organisation member

A signed-in viewer or editor in an organisation. The approval control exists so that this person cannot release or redirect a payout alone.

Scenarios
TM-03, TM-09
Findings
GUV-3GUV-10
03

Link or log holder

Whoever holds a payment link ID from a log, a ticket or a screenshot. The ID alone is meant to be useless, and today it is not.

Scenarios
TM-01, TM-16
Findings
GUV-1GUV-9
04

Same-origin script

JavaScript running on app.veltora.io while a user is signed in. It can reach the owner signing path and the session signer.

Scenarios
TM-07, TM-11
Findings
GUV-7GUV-13
05

Untrusted upstream

Relaymint, Wardkey and the RPC providers. We reviewed what this application signs and displays if one of them lies to it.

Scenarios
TM-06
Findings
GUV-6
06

Next user of a shared browser

Someone who connects the same wallet on a device where the previous user signed out.

Scenarios
TM-05
Findings
GUV-5

STRIDE enumeration coverage

  • S

    Spoofing 3 scenarios

    TM-02, TM-08, TM-13

    Forged callbacks, lookalike recipients and a body parameter that names another account

  • T

    Tampering 10 scenarios

    TM-02, TM-03, TM-04, TM-06, TM-08, TM-09, TM-10, TM-12, TM-15, TM-18

    The biggest group. What the user or approver reviewed drifting from what gets signed or paid

  • R

    Repudiation 0 scenarios

    Every money-moving action is signed by a passkey or by Relaymint and recorded by them, so no repudiation scenario survived

  • I

    Information disclosure 4 scenarios

    TM-01, TM-16, TM-17, TM-19

    Link keys reaching a log, and one user's data reaching another

  • D

    Denial of service 2 scenarios

    TM-04, TM-10

    Storage and jobs that grow or repeat without limit

  • E

    Elevation of privilege 7 scenarios

    TM-01, TM-03, TM-05, TM-07, TM-11, TM-14, TM-15

    Includes scenarios that closed clean. This strip records what we looked at, not only what we found

Interaction threat view

ABrowsertoNext.js API

Session / origin / cache

STIDE
  • Is the caller who the token says, or who the path or body claims?
  • Does anything the browser keeps after sign-out restore the session?
  • Can an anonymous caller grow a table the backup has to read?
Scenarios
TM-04, TM-05, TM-13, TM-17
Findings
GUV-4GUV-5

BNext.jstoPostgres service role

App-as-authorization

TIE
  • Does any route return a secret the row only needs to store?
  • Does any route check membership where it should check role and status?
Scenarios
TM-01, TM-03

CNext.jstoWardkey

Embedded-wallet authority

E
  • Can a caller reach another user's wallet by supplying its ID?
  • Does the owner path sign anything it is given?
Scenarios
TM-07, TM-14
Findings
GUV-7

DMachinestoRelaymint

Quote and settlement

TE
  • Is the live quote compared with the reviewed one before signing?
  • Can a refund pay twice or to another address?
Scenarios
TM-06, TM-15
Findings
GUV-6

ERelayminttoSettlement webhook

Settlement callback

ST
  • Does the route verify who sent the event?
  • Can the same event credit twice?
Scenarios
TM-02, TM-18
Findings
GUV-2

Threat scenario register

IDThreat scenarioOriginSTRIDEFlowPropertiesOutcomeEvidence
TM-01A payment link ID from a log recovers the escrow key and claims the linkBothIE06INV-09IssueGUV-1GUV-9EVT-09EVH-08
TM-02A forged settlement callback credits a ledger balance that can be withdrawnBothST04INV-11IssueGUV-2EVR-01EVT-08
TM-03A member without the approver role changes an approved payout batchInteractionET07INV-10IssueGUV-3EVT-11
TM-04Unauthenticated credential writes grow the table until the backup failsBothTD02INV-03, INV-12IssueGUV-4EVT-02
TM-05A signed-out session resumes on a shared browser with no ceremonyInteractionE01INV-02IssueGUV-5EVT-01
TM-06A reminted or tampered quote is signed as if it were the reviewed oneInteractionT05INV-06, INV-08IssueGUV-6EVT-06
TM-07The owner path signs a high-risk action the user never saw decodedBothE03INV-05IssueGUV-7EVT-04
TM-08A lookalike recipient passes the confirmation screenElementST04INV-06IssueGUV-8EVT-05
TM-09A client-computed total misstates an approved batchElementT07INV-10IssueGUV-10
TM-10Overlapping scheduler runs submit one batch twiceElementTD07INV-11IssueGUV-12EVT-12
TM-11A session signer key is reused after a wallet switchElementE03INV-02IssueGUV-13
TM-12Displayed fee or price impact diverges from what settlesElementT04, 05INV-06IssueGUV-11GUV-14
TM-13A path or body parameter selects another accountInteractionS01INV-01No issueEVH-01
TM-14A caller reaches another user's embedded wallet by supplying its IDInteractionE03INV-04No issueEVT-03EVH-02
TM-15A failed withdrawal refunds twice or to another addressInteractionTE04INV-07No issueEVT-07EVH-04
TM-16Payment link IDs can be enumeratedElementI06INV-09No issueEVH-03
TM-17A cached response is served to another userInteractionI01INV-01No issueEVT-10EVH-06
TM-18A replayed settlement event credits twice after the fixInteractionT04INV-11No issueEVT-08EVH-07
TM-19Private key material reaches a logElementI03INV-13No issueEVH-05

Annex B - Security Objectives and Invariants

13 Invariants evaluated

Across five areas. Each one is a property this codebase can make true or false on its own

4 Hold

INV-01, INV-04, INV-07 and INV-13. Not one failing case against any of them

1 Hold in part

INV-11. The main property holds; one named part of it does not

8 Do not hold

The main property itself is broken

A

Identity and authority isolation

Sessions, passkeys, embedded wallets and the owner signing path each stay tied to one account and do not drift between them.

Invariants
INV-01 to INV-05
State at baseline
2 hold, 3 do not hold
B

Intent integrity

The asset, amount, recipient and fee shown to the user are exactly what is signed and submitted.

Invariants
INV-06, INV-08
State at baseline
Both do not hold
C

Outcome containment and single effect

Refunds, payouts and ledger credits happen once, to the authorized destination, and only after the authorized approval.

Invariants
INV-07, INV-10, INV-11
State at baseline
INV-07 holds; INV-11 partially holds; INV-10 does not hold
D

Confidentiality and capability confinement

Link keys and signing material reach only the authorized principal.

Invariants
INV-09, INV-13
State at baseline
INV-09 does not hold; INV-13 holds
E

Abuse and availability

Outside input cannot grow storage or a privileged job without limit.

Invariants
INV-12
State at baseline
Does not hold

Invariant state

Holds
Nothing broken and no failing case against it.
Partially holds
The main property holds. One specific part of it does not, and we name which.
Does not hold
The main property itself is broken.
Cases pass / fail
How the contributed tests for that property split, counted per test group.

Which state a property gets depends on what failed, not on how many tests passed. INV-11 is partial because the fixed webhook now verifies and deduplicates, and the payout scheduler still has no lease.

Every property listed here passes two tests: this codebase can make it true or false on its own, and the product wants it to be true. Full wording, evidence and test links are in Annex F.

Annex C - Security Test Contribution

12 Test files

*.security.test.ts, each sitting beside the module it exercises

71 Test cases

One locked run of the security suite against the reviewed commit

43 Passing

Properties that hold, the GUV-2 fix, and current behaviour pinned

28 Failing

Open findings. They fail on purpose and go green when the fix lands

Contribution
GUV-xx means the test was written against that finding. ASSURANCE means we added it with no finding attached, to keep a property that holds from quietly regressing.
Result
Whether the case passes against the reviewed commit. A failing case goes green by itself once the production code is fixed.

The full list is in Annex F.

FlowSecurity property testedTest typeTest fileContributionResult
01Sign-out removes every session credentialNegativeauth/actions/signOutGUV-5Fail
01Responses carry no-store at both auth levelsRegressionapi/activityASSURANCEPass
02Unauthenticated credential write rejected, key size boundedBoundaryapi/passkeys/credentialsGUV-4Fail
03Cross-user wallet read is 404Negativeapi/wallets/[walletId]ASSURANCEPass
03Owner path rejects unlisted typed dataNegativeapi/wallets/[walletId]/signGUV-7Fail
04Forged and replayed settlement events rejectedRegressionapi/webhooks/settlementGUV-2Pass
04Lookalike recipient does not render identicallyInvariantsend/components/ConfirmSendGUV-8Fail
04Failed withdrawal refunds once, to the sourceInvariantwithdraw/machines/refundMachineASSURANCEPass
05Live quote matches the reviewed quoteInvariantswap/machines/swapSubmitMachineGUV-6Fail
06Link ID alone must not decryptNegativeapi/links/[linkId]GUV-1Fail
07Viewer cannot edit an approved batchNegativeapi/payouts/[batchId]GUV-3Fail

Each *.security.test.ts file sits next to the production module it exercises, and outside the normal bun test gate so it cannot break the main build. bun run test:security runs the suite. Fix the production behaviour and the assertion goes green where it is.

Coverage by flow

Security flowManual reviewRuntime testingInvariantsEvidence mappingsDomain reference
01Authentication and sessionDeep on auth actions and token verificationStaging session matrix, shared-browser replay22ASVS V2, V3
02Passkey registration and recoveryDeep on both credential routes and the backup jobOversized-write case on staging21ASVS V2, V3
03Embedded wallet and signing authorityDeep on wallet routes and the signer404 negatives against staging42ASVS V4
04Balances, send and withdrawalDeep on the webhook, send and refund machinesForged callback before the fix, replay after it; refund matrix33ASVS V4, V5
05Swap and quote executionDeep on swap machines and the Relaymint clientQuote substitution with a local proxy21ASVS V4, V5
06Payment linksDeep on both routes and the encoderSelf-test PoC against the encoder; no live link claimed11ASVS V6, V8
07Recurring payoutsDeep on payout routes and the schedulerViewer edit of an approved staging batch22ASVS V4

Annex D - Methodology Alignment

This is the approach we actually used on this engagement, not a generic template.

LayerUsed for
NIST SP 800-30 Rev. 1Risk-assessment principles: threat condition, predisposing condition, likelihood, impact. No NIST score is printed.
STRIDETwo enumeration passes: per element (the architecture map) and per interaction (the edge view). Candidates dedupe into the TM register.
OWASP ASVS 5.0Coverage cross-check after enumeration. Applicable families: V2 Auth, V3 Session, V4 Access Control, V5 Validation, V6 Crypto, V8 Data Protection.
GuvenkayaFlows, invariants, hypotheses, adversarial testing, evidence.

How the method composes

  1. 01 System model Components, stores, boundaries and flows
  2. 02 STRIDE enumeration Per element and per interaction
  3. 03 Threat scenarios Deduplicated into the TM register
  4. 04 OWASP cross-check ASVS families checked for gaps
  5. 05 Invariants and testing Adversarial review, tests contributed
  6. 06 NIST risk chain Finding, or property held with evidence

Annex E - External and Inherited Security Surface

Everything the system leans on but does not control, what each one can do to it, and what we decided about that.

ComponentWhat it does hereReview depthWhat it can do to youThe concernWhat we decided
Relaymint APIQuotes, settlement and signed callbacksTrust boundary; integration code reviewed DeepAuthors the terms the user signs and reports settlementNo reviewed-versus-live comparison (GUV-6); callback authentication fixed (GUV-2)Treat as a trust boundary the client must survive; compare locally
WardkeyEmbedded wallet custody, signing and exportTrust boundary; route integration reviewed DeepHolds the keys and enforces wallet policyOwner path unconstrained app-side (GUV-7)Allowlist on the server
Chain RPC providersBalances, nonces and broadcastTrust boundaryReport state the UI showsRPC state is used for display onlyAccept for display; settle through Relaymint
jose, relaymint-sdkInherited codeCapability actually used reviewedToken verification and the quote clientAudience and issuer enforced; client used as documentedNo change

Annex F - Evidence Appendix

Invariant register

The full wording of every security property this review set out to test, and what the evidence says about it. In the Cases column the first number is the contributed test cases that pass against the reviewed commit and the second is those that fail.

Objective domain

A
Identity and authority isolation. Sessions, passkeys, wallets and the owner path stay tied to one account.
B
Intent integrity. What the user reviewed is what gets signed and submitted.
C
Outcome containment and single effect. Refunds, payouts and credits happen once, as authorized.
D
Confidentiality and capability confinement. Link keys and signing material reach only the authorized principal.
E
Abuse and availability. Outside input cannot grow storage or a job without limit.

Each letter links to the domain card in Annex B.

IDSecurity invariantObjective domainStateCases pass / failEvidence
INV-01Session identity is taken from a verified token compared against the claimed wallet; a path or body parameter cannot select another accountAHolds9 / 0EVT-10EVH-01EVH-06
INV-02Signing out or switching wallet ends the session, and no credential the browser keeps can restore it without a new ceremonyADoes not hold1 / 3GUV-5GUV-13EVT-01
INV-03A passkey public key is stored only after a verified registration ceremonyADoes not hold0 / 4GUV-4EVT-02
INV-04Wallet routes cannot read, sign with or export another user's embedded walletAHolds6 / 0EVT-03EVH-02
INV-05The owner signing path cannot sign a high-risk action without a server-side allowlist and a decoded confirmationADoes not hold2 / 4GUV-7EVT-04
INV-06The asset, amount, recipient and fee shown to the user are exactly what is signed and submittedBDoes not hold6 / 5GUV-8GUV-6GUV-11GUV-14EVT-05EVT-06
INV-07A failed withdrawal refunds once, to the sourceCHolds5 / 0EVT-07EVH-04
INV-08The live quote a user signs matches the quote they reviewedBDoes not hold1 / 2GUV-6EVT-06
INV-09Knowledge of a payment link ID, or of logs that contain it, is not sufficient to decrypt the link or recover its escrow keyDDoes not hold3 / 4GUV-1GUV-9EVT-09EVH-03EVH-08
INV-10Only an editor changes a draft payout batch, and nobody changes a batch after approvalCDoes not hold1 / 3GUV-3GUV-10EVT-11
INV-11Ledger credits and payouts come only from authenticated events and approved batches, and each takes effect onceCPartially holds6 / 1Webhook verification and replay guard hold after the fix (GUV-2, EVR-01, EVT-08, EVH-07); the scheduler takes no lease (GUV-12, EVT-12)
INV-12Untrusted input cannot grow storage or a privileged job without limitEDoes not hold1 / 2GUV-4EVT-02
INV-13Key material and secrets do not leave through responses, logs or errorsDHolds2 / 0EVH-05

Evidence register

Every EVT row points at a real *.security.test.ts file sitting beside the code it tests. Paths are shortened: app/api/.../route.ts handlers appear as api/<path>, and the src/ and features/ prefixes are dropped.

Evidence IDTypeFlowScenarioInvariantFindingWhat it is
EVR-01Remediation04TM-02INV-11GUV-29c3e1a7f04b2d86e5a1f7c20e4d93b58a6f2c071
EVH-01Validation01TM-13INV-01Identity binding sound: the verified token account is compared to the canonicalized claimed wallet; a path or body parameter is never used to select the account
EVH-02Validation03TM-14INV-04Wallet-ID substitution: the expected wallet ID is re-derived from the caller's own link row; a foreign ID returns 404
EVH-03Validation06TM-16INV-09Enumeration closed: no listing endpoint, no owner column, and the ID space is too large to search
EVH-04Validation04TM-15INV-07Refund destination is bound to the source at request time; the refund state machine has one terminal transition
EVH-05Validation03TM-19INV-13No application code logs exported key material; the export response is ciphertext for the requester's key
EVH-06Validation01TM-17INV-01Deployed cache matrix: every API response leaves as no-store at both auth levels; cross-user probes return 401
EVH-07Validation04TM-18INV-11After the fix, the event-ID insert and the credit share one transaction under a unique constraint
EVH-08Validation06TM-01INV-09Link-claim PoC: escrow key recovered from the link ID alone and a claim signed with it; no live link claimed
EVT-01Negative01TM-05INV-02GUV-5auth/actions/signOut
EVT-02Boundary02TM-04INV-03, INV-12GUV-4api/passkeys/credentials
EVT-03Negative03TM-14INV-04api/wallets/[walletId]
EVT-04Negative03TM-07INV-05GUV-7api/wallets/[walletId]/sign
EVT-05Invariant04TM-08INV-06GUV-8send/components/ConfirmSend
EVT-06Invariant05TM-06INV-06, INV-08GUV-6swap/machines/swapSubmitMachine
EVT-07Invariant04TM-15INV-07withdraw/machines/refundMachine
EVT-08Regression04TM-02, TM-18INV-11GUV-2api/webhooks/settlement
EVT-09Negative06TM-01INV-09GUV-1GUV-9api/links/[linkId]
EVT-10Regression01TM-17INV-01api/activity cache headers
EVT-11Negative07TM-03INV-10GUV-3api/payouts/[batchId]
EVT-12Invariant07TM-10INV-11GUV-12jobs/payoutScheduler

The web version and the PDF are generated from the same source. Both carry the same signed assurance opinion.

Tell us what you need to secure.

Describe your system, main concern, and deadline. We will reply with scoping questions and a proposed next step.

Discuss your scope