GitHub

06 — History

Changelog

Protocol versions change the formats. API versions change this implementation’s HTTP surface. They move independently, and each entry says which one it is.
0.6.0

The payee check, served

PIP-0013 is implemented. A payer can ask the holder is this the person I think? before sending, and get a signed answer. The protocol version stays 0.2.0 and the API version stays 2026-10-04.genesis.

What changed

POST /payee-check is served. verify takes the name the payer expects and returns match, close_match (with the masked name), no_match or unavailable. display returns the masked name. The answer is signed and binds the key, the requester, the mode, the result and the nonce. API reference.

It is not a public read. A call is signed with a member credential, or carries a payment context: an open request or instruction issued for that address. A context for another address, or one that is paid, revoked or expired, is refused.

The name lives apart from the subject. The subject still carries no personal data. PUT /subjects/{id}/payee-record sets the registered name in payee_records, which stands in for the member's own system. payee_check: "disabled" answers unavailable on every address of the subject. A subject with no record answers unavailable too.

Allowances. Checks are budgeted per member and per payment context. The number of different names submitted for one address is limited. Every check is journaled with the proposed name hashed.

Discovery declares PATH-FINDER.PayeeCheck and advertises endpoints.payee_check.

Database

Migration 018_payee_check.sql: payee_records and payee_check_log, ring 3, RLS on, no policy, service roles only.

What to do

Nothing, unless you want payers to check names. To enable it for a subject:

PUT /api/path/v1/subjects/9f2c41a8-6d1e-4b07-9c3a-2e5f81d0a4b7/payee-record

{ "registered_name": "Alice Marie Martin" }

A payer holding a request for one of that subject's addresses then calls:

const check = await path.payeeCheck({
  address: 'path:4a91c2f7e8d3',
  endpointBase: 'https://api.member-b.com',
  mode: 'verify',
  name: 'Alice Martin',
  context: { request: '7fk2m9pq3vx8' },
});
// check.result === 'close_match', check.display_name === 'Alice M.'
0.5.0

Crossway, and a payee check

PATH resolves PATH participants. Crossway resolves destinations that live on other networks. The layer is specified in PIP-0014, implemented in @pathprotocol/sdk 0.3.0 and the API MCP server, and operated by Path Global, with access granted on request at pathglobal.finance.

The protocol version stays 0.2.0 and the API version stays 2026-10-04.genesis. Nothing a PATH operator signs changes shape.

What changed

PATH CROSSWAY is a transverse layer. Discovery says which external registry can know a key, from its form, without querying any registry. The Crossway Finder — SONAR then RESOLVER — reads that registry: whether it holds the key, at which participant, and where to send, in the rail's own beneficiary format. A payee check closes the sequence. /docs/crossway.

Crossway is not a rail, a pillar, or a directory. No value moves through it. A connector stores no registry entry beyond the answer that carried it. A Crossway answer reports a registry's entry: it carries no standing and no commitment, and it is not a gateway answer.

Connectors are attested. Every Crossway answer is signed by the connector that read the registry, and names a crossway.connector_attestation signed by Path Global: registry, operations, access, expiry. A caller verifies both signatures and discards an answer the attestation does not cover.

Raw keys are accepted, under stricter rules. Server-side only. Every SONAR, RESOLVER and payee check call states purpose: payment_preparation. No batch. Budgets per client and per registry. Where a scheme's own rules are stricter, they apply.

FINDER has a third question: the payee check. PIP-0013. Is this the person I think? verify takes the name the payer expects and returns match, close_match, no_match or unavailable; display returns the name masked. Answered by the holder, never by the network. It never returns another identifier of the subject.

A key is a way to pay someone. It is not a way to find out who they are.

Registries. pix.dict, pi-spi.alias, upi.vpa and sepa-inst.vop are named. Each opens under its own scheme's rules; GET /registries states which are reachable.

SDK 0.3.0

CrosswayClient — registries, keys, attestation, discovery, sonar, resolver, payeeCheck, finder, verify — with its own base URL and its own Path Global credential. verifyCrosswayAnswer checks an answer against its attestation without a network call.

PathClient.payeeCheck calls a holder's payee check, unsigned with a request or instruction reference, signed with a member credential otherwise.

MCP

The API server adds path_payee_check and crossway_registries, both always present, and crossway_discovery, crossway_finder, crossway_payee_check and crossway_verify when PATH_GLOBAL_KID and PATH_GLOBAL_KEY are set. A PATH member credential does not open them.

Not in this release

The reference implementation does not serve POST /payee-check: PATH-FINDER.PayeeCheck is not claimed. pi-spi is not yet a rail a capability may take; it names a registry.

What to do

Nothing, if you do not reach other networks. To use Crossway, request access, then:

import { CrosswayClient } from '@pathprotocol/sdk';

const crossway = new CrosswayClient({
  credential: { kid: 'client_2026_01', privateKeyHex: process.env.PATH_GLOBAL_KEY! },
});

const { candidates } = await crossway.discovery({ keyType: 'phone', key: '+5511987654321' });
const reach = await crossway.finder({ registry: 'pix.dict', keyType: 'phone', key: '+5511987654321' });

const { valid } = await crossway.verify(reach.resolution!);
if (!valid) throw new Error('Refusing an unattested destination');
0.4.0

One vocabulary for value, rails and amounts

Address, request and settlement objects that carry value use one vocabulary: Asset, Capability, Amount, Fee, RailReference, Party. An object in this form states grammar: 2. An object without grammar is in the 0.3 shape and is served as signed.

The protocol version of a grammar-2 object is 0.2.0. The API version that writes this form is 2026-10-04.genesis. A caller pinned to 2026-09-09.genesis writes the 0.3 bodies, signed protocol_version 0.1.0, with amount a decimal string. See Versioning.

What changed

A capability is one rail and one asset. accepts is a list of those entries. Fiat uses asset_type and an ISO 4217 asset_code. A token uses asset_type: "token", then standard and contract. rail: "blockchain" names its ledger in chain.

Rail identifiers come from a register. PIP-0007 and /docs/rails list the identifiers a capability may take. Two rails are defined here: blockchain and book. The others are reserved: the spelling is fixed, and beneficiary formats arrive with the rail's profile.

Chain identifiers come from the same register. A chain is named by slug, and completed with CAIP-2 and evm.chain_id where those exist. A listed token does not repeat decimals. An unlisted token states standard, contract and decimals.

A payment instruction says where to send this payment. POST /api/path/v1/instructions issues a signed path.instruction for one capability. The object is public when issued for a request. An instruction for an address alone takes a member credential. Lifecycle: open → used | expired | revoked. PIP-0009.

A request binds to the standing intent. The amount is an Amount. Each accepts entry may carry fees and a quote. settled_by lists the receipts that cover it. Checkout uses revoked in place of cancelled, and records each attempt. PIP-0010.

A receipt states via, sent, received and rail_references. issued_by is payer or payee. Failure and settlement reason codes are closed per side. PIP-0011. PIP-0005 route.legs[].via is a capability.

The commitment covers {grammar, address, accepts, limits}. chain.name is a display label and is not in the digest.

Breaking

A caller that writes grammar 2 pins Path-Version: 2026-10-04.genesis. The write bodies of addresses, requests, checkout and receipts change shape. Signed 0.3 objects stay as signed: they are replayed, not rebuilt, and they keep their original rail values.

A member restates its standing intent in the register's identifiers. A 0.3 commitment stays verifiable as published. A new version of the standing intent produces a new commitment.

POST /instructions is a new route. Discovery lists it. path.instruction.* and path.auth.forbidden are new error codes.

What to do

Pin the new API version, or take the latest of the genesis train. State every accepts entry as a capability. Obtain a destination with an instruction; the resolver still answers only what the address accepts.

const path = new Path({
  baseUrl,
  apiVersion: '2026-10-04.genesis',
  credential,
});

await path.setStandingIntent(address, {
  accepts: [
    {
      rail: 'blockchain',
      chain: { slug: 'base' },
      asset_type: 'token',
      asset_code: 'USDC',
      standard: 'erc20',
      contract: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
    },
    { rail: 'book', asset_type: 'fiat', asset_code: 'USD' },
  ],
});

const instruction = await path.instruction({
  request: request.reference,
  via: { rail: 'book', asset_type: 'fiat', asset_code: 'USD' },
});
0.3.0

A failure is a document, and an answer says who it speaks for

A receipt is a chain of signed statements about one settlement. status, reason_code and supersedes are inside the signature. A resolver answer names the capacity it is given in: holder or gateway.

What changed

status, reason_code and supersedes are inside the signature. The signed receipt payload changed shape. A valid signature is proof of arrival only when status is settled.

Receipts can be issued pending. POST /receipts takes status: "pending" | "settled". Absent means settled. A pending receipt is the statement from which a later transition can record failed. Issued settled, the receipt asserts success.

POST /receipts/{reference}/transitions. pending → settled | failed, settled → reversed, and nothing else. failed and reversed are terminal. reversed follows settled only. A failure never moved value; a reversal moved it and moved it back.

A transition returns a new envelope. The previous envelope stays valid and stays readable. supersedes is the SHA-256 digest of the canonical JSON of the envelope it replaces. Re-stating the state a receipt already holds returns the current envelope and appends nothing.

GET /receipts/{reference}/transitions. The whole chain, oldest first, public like the receipt. Issuance is the first link. The current envelope verifies on its own; the chain is the record of what was stated, and when.

A failure requires a reason, from a closed registry. rail_unavailable, rail_rejected, destination_unknown, destination_closed, limit_exceeded, compliance_blocked, insufficient_funds, expired. Any other value is a 400.

The list is closed so a declared rail is an undertaking. A member's accepts entry is signed, not proven. Repeated rail_unavailable on a rail that member declared is recorded under its own key, and a network rulebook can count it. See Networks.

PATH-SETTLE.Lifecycle is a new profile, declared in discovery. PATH-SETTLE.Receipts remains signed receipts verifiable by a third party. An implementation that only signs successes can claim Receipts and cannot claim Lifecycle.

path.settlement.transition_invalid (409) is a new error code.

Resolver answers

standing is in the signed payload. holder or gateway. Read it before the terms. On a gateway answer the terms, the limits and any fee belong to the intermediary, and commitment is null: the receiver has not pledged those terms. Both standings share the same shape.

Every answer this implementation emits is holder.

gateway is a block in the discovery document. It is not finder.coverage. coverage names which of a member's own index entries a broadcast search should reach. gateway names rails on which the member can reach destinations that are not its customers. The block is empty here.

A gateway is discovered through the member register, never through the directory index.

The index says who holds. The register says who reaches.

A rail footprint is information the member already publishes. Who holds a given key is other people's customers. Reading the register and calling that member's resolver is the relationship profile.

A gateway does not appear in a SONAR answer. An unknown key and a hidden key produce the same response, including on a range a member declares it can reach. PATH-FINDER.Gateway is named in the conformance register and not declared: a lookup against non-customer keys is out of scope for this version. fees, expires_at and address: null on a gateway answer are reserved.

Rules unchanged

Reciprocity budget. A member that registers nothing holds absolute_floor and no more, however much reachability it provides.

Search allowance is bought with exposure, not with usefulness.

Search of other members' customers follows from exposing one's own to the same search. Reachability by other means, including a gateway declaration, does not raise the budget.

holder_confirmation. No gateway appears in a SONAR answer, so there is no network claim of that kind to corroborate. In the relationship profile there is no network in the exchange. Holder confirmation means the destination is that member's. There is no second format.

Breaking

The signed resolver answer payload includes standing. The API version is 2026-09-09.genesis. These objects are signed protocol_version 0.1.0. amount on a request or a receipt is a decimal string beside currency. A verifier that rebuilds the payload from its own model of the object includes standing; one that replays received bytes checks the bytes it was given.

The signed receipt payload includes status, reason_code and supersedes. At issuance reason_code and supersedes are null. A failed receipt requires a reason_code from the closed registry. Envelopes issued before these fields existed still verify: stored bytes are replayed as signed and never rebuilt. A reader treats a valid signature as proof of arrival only when status is settled.

POST /receipts still rejects a body with neither source_tx_hash nor source_reference — except when status is pending, where the rail has not named the transaction yet. The reference arrives with the transition to settled.

Documentation

sdk-reference/settlement documented issueReceipt({ requestId }). The client takes requestReference, and has since 0.2.0. The page now matches the client.

What to do

Issue pending when the rail confirms later, then transition. Keep the envelope you were given — each one verifies on its own, and the chain records what was stated, and when.

const receipt = await path.issueReceipt({ /* … */ status: 'pending' });

await path.transitionReceipt(receipt.reference, {
  to: 'failed',
  reasonCode: 'rail_unavailable',
});

Read standing before the terms. Both values share the same shape. On gateway, the terms are the responder's, and commitment is null.

const answer = await path.resolver(address, endpoint);
if (answer.standing !== 'holder') { /* these terms are the responder's, not the receiver's */ }
0.2.0

One link, and a way to know who is behind it

INTEROP had two grammars, ten kinds in the URL, and no answer to the only two questions a scanner actually asks: is this a PATH code, and who stands behind it. Both are now answered, and most of the surface that existed to support the old design is gone.

What changed

One form. https://<host>/path/<reference>?n=<network>. A QR code, an NFC tag, a deeplink and an in-app handoff carry the same string.

A marker. The first path segment is exactly path. That is the whole of the offline recognition rule — no network call to tell a payment code from a link to a blog post. /p/ was rejected as a marker: it is a common prefix for product and post pages, so it produces false positives in a general-purpose scanner.

The path: scheme is withdrawn. A custom scheme fails silently when no app claims it and fails worse when several do. Operators keep their own deeplinks for their own products; PATH does not compete for that slot.

No kind in the URL. What the reference points at is a property of the object, announced by type: path.address, path.request, path.checkout, path.mandate, path.claim, path.receipt. Ten URL kinds became six object types.

No terms in the link. amount, currency and memo are rejected by buildLink. Amounts and memos belong on the signed object.

A network hint with no authority. n=<slug> replaces op=. It is a claim, not a fact — good for a cache or a waiting state, never for trust, display or routing.

A chain of trust, written down. Host → discovery → register → object → signature. Step 3 is the one implementations skip: without checking that the declared network's register lists this operator, n= and the discovery document are both self-assertions.

URI signatures are withdrawn. signUri, verifyUri, canonicalUri and the base64url sig parameter are gone. The object is signed; JSON envelopes keep hex. With no signature and no terms in the URL, a complete code is around forty characters, and the old ~300-character budget has no cause left.

One parser. parseLink replaces parseUri and parsePayload. unknown_kind is gone from the parse stage — an unfamiliar object is discovered after the fetch, on type, where an upgrade prompt is the right answer.

Discovery declares hosts[] and interop_types. Several hosts for one operator are aliases, not several operators. A host not on that list is not that issuer.

Under the hood

One reference namespace. A checkout session now has a public reference from the same generator as a request, and GET /path/<reference> serves requests, sessions, addresses and receipts alike. A session readable only at its own endpoint was not reachable by a scanned code at all.

Receipts replay the stored envelope. The bytes signed at issuance are the bytes served on read. Amounts are not reconstructed from typed columns.

PATH ID and ADDRESS, finished for what they already claimed

Attestations are real objects: issued, stored as signed, read at execution. Expiry is mandatory. Revoked or expired answers 410, not 404. Claims are closed (kyc_level, kyb_level, aml, sanctions, pep, risk_tier, identity_level 0–5).

Ownership bindings record that a subject controls an identifier, a wallet or an account. The clear value is hashed and is not stored.

Receive targets can be created, listed and revoked. The resolver ignores revoked ones. account_ref never leaves the member. Kinds now include viban, iban, ach, fps.

PATH-ID.Attestations, PATH-ID.Ownership and PATH-ADDR.Targets are declared in discovery. PATH-ID.Revocation (a published register) is still not claimed.

PIP-0005 specifies the only shape receipts.route may take. The field stays null.

Checkout is documented as live. Partial payments, concurrent attempts and refunds remain reserved.

PATH CONNECT is implemented

Discovery declares PATH-CONNECT.Core and PATH-CONNECT.Delegation: connection objects with an explicit capability set, binds_to tokens for a third party, live counters, a usage log and revocation. A token is bound to capability, amount, currency and destination. Consume requires an exact match. Spending updates the parent connection in the same write.

The consented terms are stored as signed. Status and counters move, so the object served is signed at read; consent inside it is the envelope from the moment the subject agreed.

Connections are not served at /path/{reference}. A request is public. A connection names a grantor, a grantee and a subject. Revocation is checked by the grantor when the token is used.

Other surface changes

Idempotency keys are scoped to the issuing member.

POST /sonar/confirm takes the signed SONAR answer. The caller is taken from that envelope. Cross-network confirmation does not require a credential on this member.

Batch lookups accept signed: true. That returns one envelope per key, which /sonar/confirm accepts. Unsigned remains the default. The batch is not signed as a single envelope.

SONAR answers include identifier_type and identifier_hash in the signed payload.

Member authentication covers the request body as received.

Payer-facing checkout routes require a credential. The member is taken from that credential, not from the body.

Checkout sessions can be completed. POST /checkout/sessions/{reference}/complete closes the session. It does not mark the request paid.

Payer-facing session routes take the public reference.

Request status changes are conditional writes.

Amounts are normalised on the way out to the decimal string the payer and the signature share.

Reachability.proofs.sonar is SonarAnswer | null.

Finder does not send identifiers to a resolver. Without an address, reachability stops after step 1.

Standing-intent commitments

The digest is produced when standing intent is written, stored with that version, and served at GET /commitments/{address} — the same value for every caller. The resolver quotes it and does not recompute it. The SDK compares the two with checkCommitment.

Addresses with no standing intent answer commitment: null.

A commitment is a public record of terms. It is not a defence against a fully compromised operator.

Member-local writes

Routes that act on this member's own data require this member's credential (path.auth.not_local_member otherwise). Cross-member routes — completing a checkout, consuming a connection — still accept any active member credential.

Withdrawal from the directory is scoped to this network and this member.

Conformance profiles

PATH-ID.Revocation, PATH-ADDR.Inbound and PATH-SETTLE.Reconciliation are profiles of their own. This implementation does not declare them. Standing intent is live. Receive targets are createable, listable and revocable (PATH-ADDR.Targets). Targets carry asset, chain and rail; currency is a term of the standing intent.

Receipts and checkout helpers

POST /receipts takes request_reference and signs that public reference. It requires source_tx_hash or source_reference. Issued receipts include route: null.

path.markPaid() is in the SDK.

What it breaks

Every code issued under 0.1.x. Re-issue them as https://<host>/path/<reference>.

POST /sonar/confirm takes { sonar_answer } instead of loose fields, and GET /checkout/sessions/{id} requires the issuer's credential.

POST /checkout/sessions/{id}/attempt is now {reference}/attempt, authenticated, with no body.

finder({ identifier }) alone returns accepts: [] — pass address for step 2.

GET /p/:reference is gone; it is GET /path/:reference. request.url and the new session.url already return the correct form.

kind is type on the wire: payment_request → path.request, settlement_receipt → path.receipt. Readers switching on kind need updating.

Callers importing buildUri, parseUri, parsePayload, signUri, verifyUri, issuerOrigin, canonicalUri, httpsFormLength, INTEROP_KINDS, SIGNED_STATIC_PAY_BUDGET, InteropKind or InteropUri.

POST /receipts takes request_reference, not request_id, and refuses a body with no source_tx_hash and no source_reference. Issued receipts gain route: null, so the signed payload differs — a stored envelope still verifies, since the bytes are replayed as signed.

Ring-3 routes — subjects, identifiers, addresses, standing intent, issuing requests and receipts, creating checkout sessions and connections — now answer path.auth.not_local_member to a credential belonging to another member of the network.

ResolverAnswer.commitment is string | null and gains commitment_version. Readers treating it as always-present need updating, and a payer that wants the guarantee must now fetch GET /commitments/{address} as a second call.

Receipts issued before this release cannot be served as proofs — their envelope was never stored, and no amount of reconstruction recovers signed_at. They answer path.settlement.receipt_unverifiable rather than pretending.

What to do

parseLink at the scanner, then discoveryAt(link.origin), then check the register, then readObject. buildLink({ host, reference, network }) for anything printed or tapped. Switch on object.type and treat an unknown one as an upgrade prompt.

0.1.1

INTEROP HTTPS first

INTEROP is no longer "one path: string, three transports". The grammar was already HTTPS on paper (04 §3) and path:-only in the reader. That contradiction is closed.

What changed

Canonical printed form is HTTPS. https://<host>/p/<kind>/<ref>?<params>. parsePayload dispatches https: then path:. parseUri stays strict (path: only).

path: is an app-to-app alias. op= is allowed there only. Host is the routing index on HTTPS. Removing the routing hint does not make the object unresolvable for a party that already knows the holder.

Reference class lost :. [A-Za-z0-9_.-]+. path:pay/path:4a91c2f7e8d3 is malformed. The URI carries the opaque id (4a91c2f7e8d3), not the ADDRESS spelling path:4a91c2f7e8d3.

One-segment /p/<code> is not PATH. not_path. Two segments with an unknown kind is unknown_kind, never a payment-link code.

signUri / verifyUri. Ed25519, sig in base64url (not the hex used on JSON envelopes). Canonicalisation covers kind, reference, and every parameter except sig and op. Host is not covered. Budget (~300) is measured on the HTTPS form.

issuerOrigin replaces the fictional resolveIssuerBase. The host is the index. Discovery lists hosts[] for aliases of one issuer.

Conformance profiles are PATH-INTEROP.Read / .Issue / .Pay. Pay is testable: declared accepts, a resolving rulebook, a declared certificate origin.

What it breaks

Readers and printers that emitted or accepted path:pay/path:… on a sticker. Re-issue those codes as https://<host>/p/pay/<opaque-id>.

Callers that treated https://host/p/<code> (one segment) as a PATH INTEROP URI. That remains a legacy payment link.

What to do

Use parsePayload at the scanner. Use buildUri({ host }) or signUri({ host }) for anything that will be printed or tapped. Keep parseUri for tests and for the alias only.

0.1.0

First public draft

The first published version of PATH. Draft: formats may change before v1.0, and every change will appear here with what it breaks and what to do about it.

What is specified

Five pillars. PATH ID, PATH ADDRESS, PATH REQUEST, PATH CONNECT, PATH SETTLEMENT.

PATH REQUEST is the one that may be unexpected. It exists separately because it is the only pillar that starts from the receiver — an address carries a policy (I accept USDC), a payment request carries a claim (you owe 5,000 for order 123), and systems that store both in one place end up unable to expire one or revoke the other.

Reachability in two steps. SONAR asks which member holds a key and is answered by the network; RESOLVER asks what that destination accepts and is answered by the holder. FINDER is both, composed on the client so that no single party sees the search and the answer.

One URI across three transports. QR codes, deeplinks and in-app handoffs share a grammar, a signature scheme and a reading algorithm — one anchored at both ends, so a payload that merely contains something PATH-shaped is rejected rather than mined for a first match.

Signed everything. Ed25519 over RFC 8785 canonical JSON, for any statement a counterparty may quote back.

API

2026-09-09.genesis — the first dated release of the reference implementation's HTTP surface.

The API version and the protocol version move independently: renaming a route should not bump a specification, and changing a signed format always does.

Reserved

PATH LIQUIDITY is reserved. Settlement receipts include a route field; in this version the value is null.

Selective disclosure over attestations, the full checkout lifecycle, and dispute arbitration are likewise deferred rather than forgotten.

Known limits, stated rather than buried

Identifier hashing protects the database, not members from the network. The pepper is server-side, so a member sends an identifier in the clear and the operator sees it in order to hash it. Oblivious hashing closes this and is scheduled before any network's second member; until then, nothing here will be described as zero knowledge.

A central directory observes. Short retention, a rulebook prohibition on commercial use and independent audit are the counterweights, and they are commitments rather than properties. See Privacy.

Closing a directory has costs, and they are written next to the benefits in Networks and members: adoption capped by admission, non-members left invisible, admission as a supervised power, and deterrence in place of prevention.

What ships alongside

api/Reference implementation of the HTTP surface
sdk/TypeScript client, including signature verification and URI handling
mcp/api · mcp/docsMCP servers for the API and for this documentation
db/migrations/Schema for the reference implementation

The database schema materialises the ring model directly: two schemas, two service roles, no foreign key crossing from the directory to an operator's own data, and ON DELETE RESTRICT on the index so that excluding a member does not strand their customers.

Compatibility

There is nothing to be compatible with yet. From here on, this page records every change.

Rules that will not change without a major protocol bump: the meaning of an error code, the shape of a signature payload, and the semantics of a status.