Concepts
What has been established, and what has not
This site publishes only what it can prove. Every number is read from a committed artifact at build time, and every measurement carries the environment and the date it was taken on.
This is a young project without a customer list, so the case for taking it seriously has to be made a different way. Every number on this site is read from a file in this repository, or held by a test that fails when the behaviour changes. What follows is all of it, including the two entries that did not go our way.
The three defences
A claim on this site is defended in exactly one of three ways, and anything that cannot be defended in one of them is written as an intention instead, in plain words, with no number attached.
- Derived. Counts and digests are read from the data when the page is built, so they cannot drift, because nobody types them.
- Asserted. A guarantee is held by a test that fails when the behaviour stops being true.
- Cited. A measurement points at a committed artifact and carries its scope: which path, which environment, which date.
Scope is part of the claim
"The digest survives into settlement metadata" is true and nearly useless. The same fact stated with its environment, its path and its date becomes something a reviewer can check, argue with, or reproduce. Stated without them it is a mood.
A citation names three things: the file, the path inside it, and the value shown. The build reads the path and refuses a value that disagrees with what the file holds. Two false citations reached production before that check existed, which is why it exists.
The claim ledger
What has actually been established, including the entries that did not go our way, because a page of evidence that only lists successes is a brochure.
-
A real settlement, on devnet
A Payvol request was built, signed by an external party, executed on Canton devnet, and the digest of that request travelled with the payment into the settlement metadata.
- Digest on ledger
- 96454e12c84bc20a
- Amount
- 0.1 Amulet
- Update id
- 1220178721b2d916
-
What it costs a payee to read it
Reading the settlement off the payee own participant took two Ledger API calls. There was no query store deployed on the participant that was measured, so this is the ordinary cost rather than a best case.
- API calls
- 2
- Bytes read
- 3,595
-
Who can see the metadata
The same settlement was fetched as four different parties. The payer, the payee and the instrument registry all read it. A party that took no part was told the update does not exist. That is structural to the template the transfer settles through, not a Payvol choice.
- Parties that can read it
- 3
- Parties that cannot
- 1
- Uninvolved party response
- not found
-
Where the evidence lives afterwards
After the payee accepts, the digest leaves the active contract set and survives only in the update history. An adapter that polled current state would report every settled request as unpaid.
- In active contracts after settlement
- no
- In update history
- yes
-
The conformance suite
Every accept and reject the profile pins, as versioned vectors any implementation can run. They run in your own browser on the spec page, against the built packages.
- Vectors, profile 0.1
- 42
-
The reconciliation model
Five orthogonal statuses over committed scenarios, covering exact, partial, overpaid, late, duplicate, unmatched, cross instrument refusal and ambiguous. The same input produces the same record on every run.
- Scenarios
- 17
- Status axes
- 5
-
The allocation route, attempted
An allocation settlement was attempted on devnet and could not be completed. Two blockers were recorded: there was no allocation request for the payer to allocate against, and no executor application to coordinate it.
- Settled
- no
- Decisive blockers
- 2
-
Accessibility and performance
Every route measured with Lighthouse, median of three runs, plus one axe pass. Reported here whether or not the numbers flatter us.
- Routes measured
- 13
- Lowest performance score
- 83
- Lowest accessibility score
- 96
- Open axe findings
- 15
What a property test caught
The best evidence that this approach is worth anything is not that the tests pass. It is that they once did not, and what they caught.
In August 2026 the round-trip property failed once during a full run and passed on the next
thirty-five. Rerunning it over recorded seeds put the rate at roughly one case in a
hundred thousand: four failures in four hundred thousand. Shrunk, every one of them was
the same input. A request carrying U+FEFF, an invisible character, at the start of
a field.
The decoder was deleting it. Not the encoder, which emitted the right bytes, and not the
browser, which round-trips the character correctly: our own percent-decoder, because
TextDecoder consumes a leading byte order mark unless told not to, and the option
that tells it not to is named the opposite of what it does.
The identifier moved. The canonical form is computed over the decoded object, so a request that went out as a URI and was read back did not necessarily produce the identifier it started with. That is the difference between a cosmetic bug and a correctness one, and it is why this is on the evidence page rather than in a changelog.
Both the fix and the rule are in the repository. The decoder is byte faithful now, verified across a million generated cases, and the counterexample is a permanent conformance vector in both directions, because a property test that once found a bug and has no fixture for it has forgotten what it learned.
Investigating it exposed something larger that the profile had never decided: what a field value
may contain. Twelve invisible characters were tried against both profiles. All twelve were
accepted and all twelve changed the identifier, which means two requests that render identically
to a payer could carry different ones. The draft profile now refuses them with
INVISIBLE_CHARACTER, naming the code point and the path.
This rule covers characters that are invisible. Characters that merely look alike, such as Cyrillic and Latin letterforms, are not covered by it. Those are visible, a payer can see them, and telling them apart is a wallet display concern rather than a profile rule. Saying otherwise would be claiming a protection that does not exist.
The whole investigation is written up in docs/spike/06-ROUNDTRIP.md, including the
seeds, so a stranger can reproduce it rather than take our word for it.
What we have not established
Read this before you decide. If any of these matters to your integration, it should change your timeline, and we would rather you learn it here than three weeks in.
- No independent implementation Everything that reads a Payvol request today was written by the same team. Until a second one exists, the format is documented rather than proven interoperable.
- No production deployment Every measurement on this page comes from devnet. Nothing here has processed a payment anyone actually needed.
- Key discovery is unsolved A signature can be checked, but tying a key to a company relies on infrastructure the network is still building. The wallet is told this instead of being told a merchant is verified.
- The allocation path is untested Two blockers stopped the attempt and both are recorded. Until it runs, the mapping to that settlement flow is a design, not a result.
- No external adopter No wallet, application or operations team outside this project has integrated. When one does, it will be named here rather than counted.
- Nothing is published Both packages are version 0.0.0 and npm returns a 404 for them. That is why no module on this site reads available, and it is checked by a test rather than remembered.
Check it yourself
Nothing here needs our word for it. Clone the repository, run the suite, and compare the numbers to the ones printed on this site. If a number here does not match what your machine produces, that is a bug worth reporting, and it is the kind we most want to hear about.
# the same commands we run
pnpm install && pnpm build
pnpm test
pnpm conformance