| Internet-Draft | PACT | October 2026 |
| Sharma | Expires 3 May 2027 | [Page] |
Autonomous agents can already prove who they are, show whose authority they act under, find and call one another, and pay. What no existing specification lets them do is agree on a task in a form a third party can check, deliver against it, have the delivery judged by someone other than the performer, and carry away a record of the outcome that a stranger can verify. This document specifies PACT, a set of signed JSON records that closes that gap.¶
PACT defines four things: a co-signed task contract whose digest covers its signature set; a Verdict record bound by digest to the Delivery it judges; a Facilitator-signed event trace and Outcome Record for every contract, recorded once, in one order, by a party other than the performer; and a Merkle commitment from a parent's Outcome Record to its subcontracts' Outcome Records.¶
Settlement terms are carried by reference to a profile defined outside this document. This document specifies no escrow, custody or release of value, and takes no position on the legal effect of any record it defines.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 3 May 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
By late 2026 an autonomous agent can prove who it is, show whose authority it acts under, discover another agent, call it, record what happened in a tamper-evident receipt, and pay for the call. Each of those is the subject of active standardisation, and several are specified in more detail than this document specifies anything.¶
What none of them provides is interoperability at the level of the task. Two agents built by different vendors have no common record of what one asked the other to do, no common form for the result, no way to have that result judged by a third implementation against criteria fixed before the work began, and no record of the outcome that a fourth implementation can verify without trusting any of the first three. Receipts record that an action occurred, audit records establish whether behaviour matched intent, and payment schemes move value on the payer's instruction. None of them says what was agreed, what was delivered, or whether the one met the other.¶
Those documents leave that gap on purpose, since it is outside their scope, and correctly so. It is the gap this document addresses, and only that gap.¶
PACT specifies exactly four things: a co-signed contract record whose digest covers its signature set (Section 5); a Delivery record and the Verdict record bound to it by digest (Section 6, Section 7.2); an event trace, signed by a Facilitator, from which one Outcome Record per contract is produced (Section 11, Section 12); and a Merkle commitment from a parent's Outcome Record to its children's (Section 12.2).¶
A contract names its settlement terms by reference: a profile identifier, a digest over the profile's bytes, and a parameter object that this document does not read (Section 5.3). What those terms mean, and everything about who holds or moves value under them, is the profile's to say. This document specifies the records, their digests, who signs each one, the order in which a Facilitator records events, and a commitment across records. That is the whole of it. A contract carries a price and names a settlement binding, since a task contract without them is not one; what happens to the price is the profile's, and what the binding reports is the binding's.¶
A deployment relies on other specifications, agreements or arrangements for: the meaning of the terms a contract names; agent identity and key distribution; delegation of authority from a human or organisational principal; agent discovery; transport security beyond [RFC9325]; an audit or accountability architecture; a transparency service; a payment rail or settlement network; a reputation system; and the resolution of any disagreement the records do not settle.¶
Carrying terms by reference is an old pattern in this series. ACME [RFC8555] carries a terms-of-service URL and, where a server chooses to require it, has the client assert agreement to those terms before an account is created, without defining a single term. A certificate carries its policy as an identifier whose rules live outside the IETF ([RFC5280], Section 4.2.1.4), and the framework for writing those rules [RFC3647] says it does not aim to provide legal advice. The Internet Open Trading Protocol [RFC2801] specified the messages of a trade and left the trade's terms to the parties. PACT follows that line.¶
Two mechanisms present in the -00 revision [I-D.laxsharma-pact-00] remain withdrawn: contract channels, and the sealed-bid award procedure. The reasons are recorded in [I-D.laxsharma-pact-01] and are not repeated. The changes from the -02 revision [I-D.laxsharma-pact-02] are listed in Appendix B, and that revision lists those from -01.¶
This document is Experimental, and an individual submission with no formal standing in the standards process: no working group has adopted it and the IETF has not endorsed it. The question it tests is stated over protocol observables only. Given the same sequence of posted records, the same clock readings and the same reports from the settlement binding, two independent Facilitator implementations should produce the same event trace (Section 4.2). Given the same trace and the same terms profile, they should produce the same Outcome Record body (Section 12), byte for byte after canonicalization. The experiment succeeds if two independent Facilitators, serving Buyers and Sellers built by different implementers, reach every terminal state in Figure 2 with Outcome Records either can verify and that agree. It fails, and that would itself be worth recording, if the trace turns out to under-determine the outcome, which is to say if two honest implementations reading the same records disagree about what happened. Experience should be reported to the author and to the repository named in Section 16. The non-normative profile in Appendix A exists so that the experiment can be run before any other profile is written.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
Canonical form. Every JSON object defined here is canonicalized with
JCS [RFC8785] before hashing or signing. Implementations
MUST order object keys by UTF-16 code unit as
[RFC8785] Section 3.2.3 requires. Sorting by Unicode code
point is a common substitution; it agrees with the required order until a key outside the Basic Multilingual Plane is compared with one whose first differing unit lies in U+E000 to U+FFFF, where the two orders disagree. Numbers
MUST be serialized as [RFC8785] Section 3.2.2.3
requires, which is how ECMAScript prints them: the number one is
1, whatever type held it, and never 1.0.¶
Figures. A figure line that would exceed the page width is folded with the single backslash strategy of [RFC8792], and a figure that contains a fold begins with the header line that RFC requires. The figure is read after unfolding; the digests it prints are whole once the fold is removed.¶
Object digest. The digest of an object is the string
sha256: followed by the lowercase hexadecimal SHA-256 of the
canonical form of the whole object, including every signature member it
carries. Every hash member in this document that names another object
(vtc_hash, delivery_hash, challenge_hash,
the object member of a trace entry) is that object's digest, and an element of the list D in Section 12.2 is the 32 bytes that digest's hexadecimal encodes. A digest that
excluded signatures would prove what was written and not who agreed to
it; the -00 revision had that defect and the -01 revision fixed it for
the contract only. This revision applies one construction
everywhere.¶
Signing input. A signature over an object is computed over the
canonical form of the object with the signing member (signature
or signatures) removed, as Section 14.1
specifies. The digest of an object and the signing input of an object
are therefore different byte strings, and the difference is the
signature set.¶
Version. The pact member carries a version of the form
major.minor; this document defines 0.3. Every object defined here is
hash-committed and signed, so a member an implementation does not
recognise is inside the commitment and cannot be ignored safely. An
implementation MUST reject an object whose pact version it
does not implement, and MUST reject an object carrying a member this
document does not define for it, with two exceptions: the contents of terms.parameters (Section 5.3), which the named profile defines and this document does not read; and the members of a Delivery's evidence, a Challenge's proof and a TaskSpec's constraints beyond those Section 3 names, which the verification profile defines. Extension is by a
new version, not by adding members. An implementation MUST also
reject an object that omits a member this document requires in it
(Section 3, and for a trace entry
Table 2).¶
Time. Every timestamp is an RFC 3339 date-time [RFC3339] in UTC with the "Z" designator. The Facilitator's clock governs every deadline and window in this document: the instant at which the Facilitator records an event is the instant that counts, that instant is what the trace carries, and parties SHOULD allow for skew when acting near a boundary. Section 17.1 says what that clock can and cannot prove.¶
Amounts. An amount is a decimal string with no sign, no exponent and a
fractional part of two to eighteen digits; comparisons are exact and
no rounding is implied. A currency is an asset identifier whose
namespace is defined by the settlement binding named in
price.settlement, and need not be an ISO 4217 code. A network is a ledger identifier in the form the same binding defines; the examples use [CAIP-2] chain identifiers. This
document carries amounts; it does not say what any amount is for. Where
a record produced under this document lists amounts, as
terms_result does (Section 12.1), the meaning
of every entry is the named profile's.¶
Identifiers. A party identifier is a URI [RFC3986] in one of the two forms of Section 14.1.1, and so is written in ASCII. Two identifiers are equivalent when they are equal after the normalization in Section 9.1, and every comparison of identifiers in this document is made after that normalization.¶
Five words in this document have meanings elsewhere that are close enough to mislead, and are defined here once.¶
evidence member of a Delivery is
the set of artefacts a Verifier evaluates, produced by the Seller.
It is not Evidence in the sense of [RFC9334]. The member name is kept because it is the ordinary word for what the member holds; the RATS term names a role in an attestation architecture, and this note is the disambiguation.¶
The remaining roles are defined by what they sign and receive in Section 3.10, and the objects by their members in Section 3.¶
This section lists every member this document defines, by the object that carries it, with its type, whether it is required in that object, and what it commits to. It is a dictionary and not a rulebook: the rule that a record omitting a required member, or carrying one this document does not define for it, does not conform is stated once, in Section 2; the rules a Facilitator applies when it accepts or refuses a record are in Section 14 and in the section that defines the record. No sentence in this section requires anything of any party. Where a member's meaning is the named terms profile's, the entry says so and says nothing more.¶
Types are JSON types. A digest is a string of the form in Section 2. An amount is a string of the form in Section 2. A URI is a string. A timestamp is a string of the form in Section 2. Cardinality is written as required or optional.¶
pact:0.3 for objects defined by this document.¶
type:VerifiableTaskContract, Delivery,
Verdict, Challenge, ContractStatus,
OutcomeRecord, or FacilitatorCapabilities.¶
signature:signatures:Carried in the Verifiable Task Contract (Section 5),
media type application/vnd.pact.contract+json.¶
id:parties.facilitator, and neither "." nor "..", which
resolution of {contract}/{id}
(Section 13) would remove.¶
parties:buyer (URI, required),
seller (URI, required), facilitator (URI,
required), verifier (URI, optional). Commits to who plays
each role for this contract.¶
task:spec_hash
(digest, required) commits to a TaskSpec (Section 5.2);
spec_uri (URI, optional) says where its bytes may be
fetched; deadline (timestamp, required) is the instant at or after which the deadline-passed event may be recorded
(Section 4.2).¶
price:amount
(amount, required), currency (string, required),
settlement (URI, required, naming a settlement binding),
network (string, required, in the form the binding
defines). Commits to a figure and a venue that both parties signed.
The meaning of the figure is the named terms profile's.¶
verification:tier
(string, required), profile (string or URI, required;
Section 9), criteria_hash (digest,
required; the manifest digest of the acceptance instrument per
Section 5.1), max_verdict_seconds (integer, required, greater than zero; the longest interval after delivered within
which a first Verdict is recorded before verdict-lapsed
may be). Commits to how a
Delivery is judged and by what.¶
flow:verdict-first, delivery-first, no-window
(Section 7.1). Selects the shape of the state machine
for this contract.¶
terms:profile (URI, required) names
a terms profile; profile_hash (digest, required) commits
to the profile's bytes as Section 5.3 says;
parameters (object, required, may be empty) carries the
profile's parameters. This document reads no member of
parameters; every one of them means what the named
profile says.¶
challenge:window_seconds (integer, required, greater than zero) is the
duration of the challenge window; max_dispute_seconds (integer, required, greater than zero) is the longest interval after a
challenge event within which a Verdict on that Challenge
is recorded before dispute-lapsed may be.¶
parent:vtc_id (string,
required), vtc_hash (digest, required), and
facilitator (URI, required) identify the parent contract
and the Facilitator that holds it.¶
The TaskSpec is the content committed by task.spec_hash
(Section 5.2). It is not transmitted over the
endpoints of this document.¶
description:inputs:schema_uri
with schema_hash, and where a representative sample is
published, sample_uri with sample_hash; each URI
with its digest over the dereferenced bytes.¶
deliverable:format
(string) and schema_uri with schema_hash.¶
acceptance:harness_uri with harness_hash for
re-execution tiers, enclave and model policy for attestation tiers,
a proof statement with its verifying key for proving tiers, or
rubric_uri with rubric_hash for judgment tiers;
plus thresholds (object) in machine-readable form.
harness_hash equals the contract's
criteria_hash.¶
constraints:acceptance only; whether a
verification profile reads any of it is a statement that profile
makes (Section 9).¶
Carried in the Delivery (Section 6), media type
application/vnd.pact.delivery+json.¶
vtc_id, vtc_hash:work_hash:work_uri:input_hash:evidence:verification.tier and verification.profile; for the acceptance profile, profile, instrument_hash and results_hash (required) and results_uri (optional). Conformance to the profile is a validity
condition of the Delivery, not a judgement on the work.¶
Carried in the Verdict (Section 7.2), media type
application/vnd.pact.verdict+json.¶
vtc_id:delivery_hash:challenge_hash:outcome:PASS or
FAIL.¶
profile, instrument_hash:criteria_hash.¶
results_hash:acceptance, the bytes of the results file the instrument wrote.¶
evaluated_at:Carried in the Challenge (Section 7.3), media type
application/vnd.pact.challenge+json.¶
vtc_id, delivery_hash:proof:verification.profile; for the acceptance profile, profile, instrument_hash and results_hash (required), results_uri and failing_checks (array of strings; optional).¶
costs:amount and
currency: an amount the Challenger states. This document records it in the trace and reads only
the form of its amount (Section 14.2); its meaning is the named terms profile's.¶
Carried in the Contract Status (Section 11), media
type application/vnd.pact.status+json, the Facilitator's
signed response to every accepted POST.¶
vtc_id, vtc_hash:state:trace:event (string, required), at
(timestamp, required), object (digest, required on the events for which Table 2 names it), and the event-specific
members listed in Section 4.2.¶
issued_at:Carried in the Outcome Record (Section 12), media
type application/vnd.pact.outcome+json.¶
vtc_id, vtc_hash:parties:parties object, copied, so that the record names its
subjects and which side of the contract each was on.¶
task:spec_hash
(digest, required): the contract's task.spec_hash,
copied. No other member of the contract's task is
copied.¶
verification:verification object, copied, so that the record
says what judged the work and against what.¶
flow:flow, copied.¶
outcome:state
(string, required; FINAL, SETTLED or
ABANDONED), challenge_upheld (boolean,
required) and verdict (string, required): the digest of
the Verdict standing at the terminal entry, or
verdict-lapsed, or none
(Section 12).¶
work_hash:delivered entry: the work_hash of the
Delivery that entry names. Binds the record to what was
produced.¶
trace:terminal event.¶
terms_result:profile and
profile_hash (copied from the contract), currency
(string), and transfers (array of objects), each with event (integer, the zero-based index of the trace entry the transfer follows), from (string), to (string), amount
(amount) and code (string). The entries are the named
profile's output for the trace; this document defines their form
and two arithmetic invariants over them, and nothing about their
meaning.¶
children_merkle_root:Carried in the Facilitator capability document
(Section 8), media type
application/vnd.pact.facilitator+json.¶
facilitator:parties.facilitator.¶
issued_at:settlement_bindings:id (URI), networks and assets (arrays of strings), all required.¶
flows:verdict-first among them.¶
verification_profiles:terms_profiles:id (URI) and
profile_hash (digest): the terms profiles, at the
revisions named, whose schedules this Facilitator evaluates.¶
max_contract_value:amount and currency; a contract whose price, stated in the same currency, exceeds it is refused, as is one whose price is stated in another currency (Section 13.1).¶
challenge_deposit:amount and currency; see
Section 7.3.¶
retrieval:parties,
the default of Section 17.12, or open.¶
endpoints:A role is defined by where its identifier appears, what it signs, and what it receives. Nothing else about a role is defined here.¶
| Role | Identifier appears in | Signs | Receives |
|---|---|---|---|
| Buyer |
parties.buyer
|
the contract | Contract Status, Outcome Record |
| Seller |
parties.seller
|
the contract; the Delivery; as the Buyer of a child, that child's contract (Section 10.2) | Contract Status, Outcome Record |
| Facilitator |
parties.facilitator,
parent.facilitator, the capability document |
Contract Status, Outcome Record, the capability document | every posted record |
| Verifier |
parties.verifier, or the
kid of a Verdict |
the Verdict | the Delivery and, on a Challenge, the Challenge |
| Challenger | the kid of a Challenge |
the Challenge | Contract Status |
One identifier may play more than one role across contracts, and Section 9.1 and Section 14.2 say which combinations within one contract a Facilitator refuses.¶
A contract passes through four phases: Propose establishes the record, Agree co-signs it and a Facilitator accepts it, Complete produces a Delivery and a Verdict on it, and Record produces an Outcome Record. Every step after Agree is an event the Facilitator records on its own clock, in one order, and the sequence of those events is the contract's trace. The trace is the protocol's central object: the state machine is defined over it, every response a Facilitator gives carries the prefix recorded so far, and the Outcome Record carries the whole of it.¶
Buyer Facilitator Seller Verifier | | | | |<==== contract negotiated and co-signed ==>| | | | | | |-- POST contract ->| | | |<-- Status --------| [ accepted ] | | | | [ funded ] | | | | | | | | [ Seller performs ] | | |<-- POST Delivery --| | | |-- Status --------->| [ delivered ] | | | | | | |---- Delivery, criteria_hash ------->| | |<--- POST Verdict -------------------| | |---- Status ------------------------>| | | [ verdict PASS ] [ window-opened ] | | [ window-closed ] [ children-final ] | | [ terminal FINAL ] | | | | | |<-- Outcome Record-|-- Outcome Record ->| |
Every accepted POST is answered with a Contract Status (Section 11), a Facilitator-signed object carrying the state and the trace so far. Nothing in the figure moves value, and no arrow in it is named for a movement of value. What a terms profile does at each bracketed event is the profile's, and it is reported once, in the Outcome Record, as a list the profile produced and the Facilitator signed.¶
ACCEPTED -funded-> FUNDED -delivered-> DELIVERED
| | |
| deadline- | deadline- | window-opened
| passed | passed v
| | WINDOW_OPEN <------+
| | | | |
| | challenge | | window | verdict PASS
| | v | closed | on it, or
| | DISPUTED----|--------+ dispute-
| | | | lapsed
| | verdict | |
| | FAIL | |
v v v v
+--------------------------------------------------+
| AWAITING_CHILDREN |
+--------------------------------------------------+
| children-final, then terminal
v
FINAL SETTLED ABANDONED
The figure omits three arrows that the table carries: a FAIL
Verdict recorded in DELIVERED or in WINDOW_OPEN also leads to
AWAITING_CHILDREN; under the no-window flow delivered leads there directly; and a Verdict that is late (verdict-lapsed)
opens the window without one. FINAL, SETTLED and ABANDONED are
terminal and each produces exactly one Outcome Record. The -01 revision had a state, RELEASING, named for a movement of value; it is gone. FUNDED remains, named for the event the settlement binding reports (Section 4.2), and nothing here says what that report means.¶
The state named PROPOSED in earlier revisions is gone. Between the parties' signatures and the Facilitator's acceptance a contract exists only on the parties' side, so no Facilitator could observe that state, and the -01 reference implementation never reported it, although the -01 Section 12.1 example printed it in a 201 response.¶
A trace entry is a JSON object with event (one of the
names below), at (the Facilitator's clock when it was
recorded), object where the table names one (the digest,
Section 2, of the record the table names), and
the members listed for the event, each required unless the table
marks it optional or states when it is present. A Facilitator MUST
record the entries of one
contract in the order it recorded them and MUST NOT reorder, remove
or alter an entry once a Status carrying it has been issued
(Section 17.1 says what that rule does and does
not prove).¶
| Event | Recorded in; then. Members and condition |
|---|---|
accepted
|
none; then ACCEPTED. object is vtc_hash. The contract passed Section 13.1. |
funded
|
ACCEPTED; then FUNDED. ref (string, optional, in the form the settlement binding defines). Recorded when the settlement binding named in price.settlement reports that whatever the named terms profile requires before work starts is in place; how a Facilitator observes that is the binding's to say. Where the profile requires nothing, funded follows accepted in the same operation. |
deadline-passed
|
ACCEPTED or FUNDED; then AWAITING_CHILDREN. task.deadline has passed with no delivered entry. |
delivered
|
FUNDED; then DELIVERED, or under no-window AWAITING_CHILDREN directly. object is the Delivery's digest. The Delivery passed Section 6. |
window-opened
|
DELIVERED; then WINDOW_OPEN. Recorded in the same operation as the entry it follows, with the same at: under delivery-first the delivered entry; under verdict-first a PASS verdict or verdict-lapsed. closes_at (timestamp, required) is at plus challenge.window_seconds. |
verdict
|
DELIVERED (verdict-first, no Verdict standing), WINDOW_OPEN (delivery-first, no Verdict standing) or DISPUTED (answering a pending Challenge); then as the condition says. object is the Verdict's digest; signer (the kid of its signature); outcome (PASS or FAIL); answers (digest of the Challenge, present exactly when the Verdict carries challenge_hash); supersedes (digest of the Verdict that stood, present exactly when one did). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes the state of nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending. |
verdict-lapsed
|
DELIVERED; unchanged. Under verdict-first, verification.max_verdict_seconds have passed since delivered with no verdict. window-opened follows in the same operation. |
challenge
|
WINDOW_OPEN or DISPUTED; then DISPUTED. object is the Challenge's digest; signer (the kid of its signature); costs copied from the Challenge when present. The Challenge passed Section 7.3 before closes_at. |
dispute-lapsed
|
DISPUTED; then WINDOW_OPEN. object is the Challenge's digest. challenge.max_dispute_seconds have passed since that challenge entry with no Verdict answering it. Leads to WINDOW_OPEN once no Challenge is pending; the earlier Verdict, if any, stands. |
window-closed
|
WINDOW_OPEN; then AWAITING_CHILDREN. closes_at has passed and no Challenge is pending. The window is never extended: a dispute that outlasts it delays this entry and does not move closes_at. |
child-registered
|
any non-terminal; unchanged. object is the child contract's digest; facilitator (URI). Section 10.2. |
child-final
|
any non-terminal; unchanged. object is the child's Outcome Record digest; child (the child contract's digest). |
child-unresolved
|
any non-terminal; unchanged. child (the child contract's digest). The child's latest finality instant (Section 10.3) has passed and no Outcome Record for it is held. |
children-final
|
AWAITING_CHILDREN; then terminal follows. Every registered child has a child-final or child-unresolved entry. A contract with no registered children records this entry on entering AWAITING_CHILDREN. |
terminal
|
AWAITING_CHILDREN; then FINAL, SETTLED or ABANDONED. state (the terminal state) and challenge_upheld (boolean). ABANDONED where deadline-passed was recorded; SETTLED where the standing Verdict is FAIL, with challenge_upheld true when that Verdict answers a Challenge; FINAL otherwise. |
The standing Verdict is the last verdict entry in the
trace that no later entry supersedes. A Challenge is pending from its
challenge entry until a verdict entry answers it, a dispute-lapsed entry names it, or a terminal entry is recorded.¶
Every instant in the table is read from the Facilitator's clock, and an entry conditioned on an instant having passed is recorded at the first opportunity after it, which need not be that instant. Two Facilitators given the same posted records, the same clock readings and the same reports from the settlement binding record the same trace; that is the determinism the experiment in Section 1.4 tests, and the reason every condition above is stated over the trace, the clock and the binding's report, and nothing else.¶
An instant has passed when the Facilitator's clock reads that
instant or a later one, and a record received when the clock reads an
instant has arrived at it, so a Challenge received when the clock
reads closes_at is after the window. Before issuing a
Status or acting on a posted record a Facilitator MUST first record
every clock-driven entry that is due, so that the Status is issued
and the record judged in the state the clock produced; entries due at
the same instant are recorded in the order of the table. The at of an entry MUST be no earlier than that of
the entry before it, and a Status's issued_at MUST be no
earlier than the at of its last entry.¶
A VTC is a JSON object, media type
application/vnd.pact.contract+json, with the members in
Section 3.2. A VTC is valid only if every required member is present, the Buyer and the Seller are distinct after normalization (Section 14.2), any named Verifier satisfies Section 9.1, and both the Buyer and the Seller
have contributed exactly one signature that verifies against a key bound
to its identifier (Section 14.2). The Facilitator and any
Verifier do not sign the VTC; their assent is expressed by acting on it,
and a Facilitator that will not act on a contract refuses it at
Section 13.1.¶
The settlement identifier, the network and the asset are all
carried inside price so that a co-signed VTC is bound to one
venue. The -00 revision omitted them, which made a signed contract
replayable against any facilitator, chain or token contract.¶
========== NOTE: '\' line wrapping per RFC 8792 ===========
{
"pact": "0.3",
"type": "VerifiableTaskContract",
"id": "vtc_9f2c11",
"parties": {
"buyer": "did:web:buyer.example:agents:procure-1",
"seller": "did:web:dataforge.example:agents:etl-3",
"facilitator": "did:web:settle.example",
"verifier": "did:web:audit.example"
},
"task": {
"spec_hash": "sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef11\
1d27ff6bee37f05531823b72",
"spec_uri": "https://buyer.example/specs/taskspec.json",
"deadline": "2026-11-14T00:00:00Z"
},
"price": {
"amount": "180.00",
"currency": "USDC",
"settlement": "https://settle.example/bindings/ledger-1",
"network": "eip155:8453"
},
"verification": {
"tier": "T0-reexec",
"profile": "acceptance",
"criteria_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\
10b8351c9c55eb9316416265fc1b",
"max_verdict_seconds": 86400
},
"flow": "verdict-first",
"terms": {
"profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restit\
ution",
"profile_hash": "sha256:c6b85fc09b885fb082e79a2e833356628e77c\
18a3211f81fc0accf8b07057192",
"parameters": {
"...": "the profile's; not read here"
}
},
"challenge": {
"window_seconds": 3600,
"max_dispute_seconds": 86400
},
"signatures": [
{
"protected": "...",
"signature": "..."
},
{
"protected": "...",
"signature": "..."
}
]
}
The values are the reference repository's
(Section 15), with the signatures abbreviated. The
parameters object is shown elided on purpose: nothing in this
document depends on what is in it.¶
Every URI carried inside hash-committed content MUST be accompanied
by a sibling hash over the dereferenced bytes. The -00 revision
committed harness_uri as a string while leaving the bytes at
that URI uncommitted, which permitted a Buyer to substitute the
acceptance instrument after signature, run the substituted
instrument, and submit the failure as a valid proof of nonconformance. The -01
revision stated the rule and its own reference TaskSpec broke it for
three of four URIs; this revision's example carries all four
sibling hashes, and the validator checks each.¶
Where the committed content is a bundle of files rather than a
single octet stream, the commitment MUST be computed as
SHA-256(JCS(M)) where M is an object mapping each file's
path, relative to the bundle root and expressed with "/" separators,
to the digest of its bytes in the string form of Section 2, over every file in the bundle. A file whose name, or any directory on whose path, begins with a dot is not part of a bundle. A
manifest of per-file digests is specified rather than an archive
digest because archive formats carry ordering, timestamp and
permission metadata that is not stable across producers. The same
construction commits to a terms profile (Section 5.3).¶
The content committed by spec_hash is a TaskSpec: a JSON
object with the members in Section 3.3,
canonicalized per [RFC8785] before hashing. It is not
transmitted over the endpoints of this document; the parties exchange
it before signing, and spec_uri may say where.¶
The acceptance object MUST carry the members required for
the contract's tier: harness_uri and harness_hash
for re-execution tiers, enclave and model policy for attestation
tiers, a proof statement with its verifying key for proving tiers, or
rubric_uri and rubric_hash for judgment tiers. An
empty acceptance object MUST be rejected. The -00 revision's
schema permitted one, which made every proof of nonconformance impossible.¶
Thresholds MUST be stated so that they cannot be satisfied by returning almost nothing. A threshold expressed only as a rate over returned rows is satisfied by returning one correct row out of millions; a completeness condition relative to the committed input is therefore required wherever the deliverable is a transformation of that input.¶
The terms member names the settlement terms both parties
signed, by reference. It carries a profile URI, a
profile_hash, and a parameters object. This document
defines no obligation between parties and takes no position on the
legal effect of any object it defines; what the named profile says the
parties have agreed to, and what any of it means between them, is the
profile's and its authors' to say.¶
profile_hash is the manifest digest of
Section 5.1 over the profile's bundle. A bundle usable
with this document contains at least three files: the profile's prose,
parameters.schema.json, a JSON Schema
[I-D.bhutton-json-schema] for the parameters object, and
vectors.json, whose form Section 12.1
gives. A digest over prose alone would commit the parties to bytes and
not to behaviour; the schema and the vectors are what make two
implementations of the profile checkable against each other.¶
A Facilitator MUST refuse a contract whose terms.profile
and terms.profile_hash do not match an entry in the
terms_profiles array of its own capability document
(Section 8; terms-unsupported), so that no party signs terms the Facilitator will not evaluate, and MUST refuse a contract whose parameters do not validate against the named profile's parameters.schema.json (terms-parameters-invalid). This document reads parameters for no other purpose; the named profile's schedule and admission rule read them as the profile's. The rule of Section 2 that an
undefined member is rejected does not apply inside
parameters; the profile's schema governs there.¶
A profile usable with this document defines, in its prose, a
schedule: a total, deterministic function from a contract and a trace
prefix (Section 4.2) to the list of entries the profile
emits at the last event of that prefix, in the form of
Section 12.1. Total means every event in
Table 2 has a defined result, including the ones a
profile author would rather not think about: a lapsed dispute, an
unresolved child, a contract abandoned before it was funded.
Deterministic means the result depends on the contract, the trace and
nothing else, so that any party holding those can recompute it. The prose also names the accounts the schedule moves value between, and says which of them are internal, opened empty and required to close empty, and which are external. A profile MAY also state
an admission rule: a condition on the contract, evaluated once at
accepted, whose failure is reported with a problem type from
the profile's namespace (Section 13.3). This document does not
register profiles and defines none normatively;
Appendix A carries one for the experiment.¶
Everything the -01 revision said in its Section 5.3, and everything
it said in its Section 7 about what is posted, released,
returned or forfeited and when, is now the content of a profile. The
member that carried those figures inside the contract is gone; the
figures a profile needs are in parameters, and the -01
figures in particular are the parameters of the profile in
Appendix A.¶
The Delivery is the record a contract is judged against. It is a
JSON object, media type application/vnd.pact.delivery+json,
with the members in Section 3.4, signed once by the
Seller.¶
A Facilitator MUST refuse a Delivery, with the problem type named,
when: its vtc_hash does not match the contract
(object-conflict); the contract is not in FUNDED
(wrong-state); its signature does not verify against a key
bound to parties.seller (signature-invalid,
unexpected-signer); its evidence member is absent or
does not conform to the verification profile named in the contract
(evidence-nonconformant); its evidence names an
instrument other than the one the contract committed to, which under
the acceptance profile is an instrument_hash that
is not verification.criteria_hash
(evidence-nonconformant); or input_hash is absent
where the tier re-executes (evidence-nonconformant). A refused
Delivery is recorded in no trace; the contract stays in FUNDED and a
conformant Delivery may follow before the deadline. The -01 revision
treated a nonconformant Delivery as a FAIL Verdict, which decided a
question about value inside a rule about shape; the consequence of a
Seller reaching the deadline with nothing conformant recorded is now
the deadline-passed event, and what that event costs anyone is
the profile's.¶
Conformance of evidence is a check on shape, not on
substance: the Facilitator confirms that the members the profile
requires are present and well formed, and nothing about whether the
work is any good. That is why the check stays on the right side of the line
drawn in Section 2.1. Where task.deadline passes
with no delivered entry, the Facilitator records
deadline-passed (Section 4.2). No window opens,
because there is nothing to challenge.¶
========== NOTE: '\' line wrapping per RFC 8792 ===========
{
"pact": "0.3",
"type": "Delivery",
"vtc_id": "vtc_9f2c11",
"vtc_hash": "sha256:2db525ce98c8ee4b4cb63785450439487513ef2a2d9\
5a87de407e2f4717a53f0",
"work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee2\
80a3170b6b9a5037036ef0",
"work_uri": "https://cdn.dataforge.example/o/a26d",
"input_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c9\
75a1d1ce519d582306338b1",
"evidence": {
"profile": "acceptance",
"instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\
5c10b8351c9c55eb9316416265fc1b",
"results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf84\
8ebaca540214b4f6d2165688a51",
"results_uri": "https://cdn.dataforge.example/o/28d3"
},
"signature": {
"protected": "...",
"signature": "..."
}
}
The -01 revision said that a Buyer countersignature over the
Delivery constituted a receipt. The Delivery's signing member is a
single object, so no second signature could be carried, and the
sentence is withdrawn. A Buyer that wants a record of receipt has one:
the Status the Facilitator returns for the Delivery carries the
delivered entry and the Facilitator's signature over it.¶
The flow member selects one of three shapes for the state
machine of Section 4.1. A conformant Facilitator MUST implement verdict-first and MUST list it in the flows member of its capability document (Section 3.9); the others are OPTIONAL, and a
Facilitator MUST refuse a contract naming a flow it does not advertise
(flow-unsupported).¶
verdict-first:verdict-lapsed; a FAIL Verdict ends the contract without a
window.¶
delivery-first:delivered. One Verdict MAY be recorded inside the window without a Challenge, and a second only in answer to one; a FAIL ends the contract, a PASS changes nothing.¶
no-window:delivered is followed by the terminal
path.¶
The -01 revision had four release modes. Two of them produce the same trace and differed only in which event a profile acts on, which is a profile parameter and not a protocol matter. The mapping is in Appendix A.2.¶
The window opens at the instant of the window-opened entry
and closes at that instant plus challenge.window_seconds,
carried in the entry as closes_at. A Facilitator MUST NOT
accept a Challenge after closes_at, MUST NOT extend the
window for any reason, and MUST NOT record window-closed
while a Challenge is pending.¶
A Verdict is a signed statement that a Delivery was evaluated
against the committed instrument, and with what outcome. It is a JSON
object, media type application/vnd.pact.verdict+json, with
the members in Section 3.5, signed once.¶
========== NOTE: '\' line wrapping per RFC 8792 ===========
{
"pact": "0.3",
"type": "Verdict",
"vtc_id": "vtc_9f2c11",
"delivery_hash": "sha256:dfabe6a5d1aaf23868dec65e1517dc0dfa911b\
41054ea10d641bb919aed34c95",
"outcome": "PASS",
"profile": "acceptance",
"instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\
10b8351c9c55eb9316416265fc1b",
"results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf848e\
baca540214b4f6d2165688a51",
"evaluated_at": "2026-11-10T09:14:22Z",
"signature": {
"protected": "...",
"signature": "..."
}
}
The Verifier is the party identified by the kid of the
Verdict's signature. Where the contract names
parties.verifier, a Facilitator MUST refuse a Verdict signed
by any other party; otherwise it MUST refuse a Verdict whose signer
does not satisfy Section 9.1
(verifier-not-independent). It MUST refuse a Verdict for a
contract with no delivered entry
(no-recorded-delivery); one whose delivery_hash
does not match that entry, or whose profile or
instrument_hash does not match the contract
(verdict-nonconformant); one received in a state, or under conditions, that the table in Section 4.2 does not list for it, or under the
no-window flow (wrong-state); and one carrying
challenge_hash that names no pending Challenge, or omitting
it while the contract is DISPUTED (verdict-nonconformant).
A Verdict recorded while one stands supersedes it, and both stay in the trace; since a Verdict is accepted while one stands only in DISPUTED, only a Verdict that answers a Challenge ever supersedes.¶
A Verdict commits to the instrument it ran and to the results it
produced. Without instrument_hash a Verifier could run
something other than the committed instrument and the contract would
have no way to tell; that is the substitution attack of
Section 17.4, arriving from the verification
side. A Verdict reaches the delivered bytes through the Delivery:
its delivery_hash commits to the Delivery, signature
included, and the Delivery's work_hash commits to the
bytes, or to their manifest where the deliverable is a bundle
(Section 5.1), so a reader who holds both records can
tell which bytes were judged.¶
Under verdict-first a Verifier that never answers would
leave a contract in DELIVERED forever, and the -01 revision had no
rule for it. verification.max_verdict_seconds bounds the
wait: when it passes with no Verdict, the Facilitator records
verdict-lapsed and opens the window, so that the contract can
still be challenged and can still end. What a lapsed Verdict costs
anyone is the profile's.¶
A Challenge is a JSON object, media type
application/vnd.pact.challenge+json, with the members in
Section 3.6, by which a party submits a proof of nonconformance (what optimistic systems call a fraud proof) inside the window. A Facilitator MUST refuse a Challenge
received when the contract is not in WINDOW_OPEN or DISPUTED, or
after closes_at (challenge-window-closed); one
whose delivery_hash does not match the delivered
entry (object-conflict); one whose proof does not
conform to the verification profile (proof-nonconformant);
one whose signer it cannot resolve (signature-invalid); and
one signed by the contract's Seller (unexpected-signer),
since a performer's statement against its own Delivery is not a
proof of nonconformance and the -01 revision left the case open. A Facilitator
MUST NOT refuse a Challenge on the ground that its signer is the
contract's Buyer.¶
A Challenge that is accepted is evaluated by a party satisfying
Section 9.1, whose finding is a Verdict carrying challenge_hash; the Challenger's own assertion is not a finding, unless the Challenger is the verifier the contract names, whose Verdict is the finding by definition. A named Verifier that finds its own PASS wrong posts a Challenge and answers it. The Challenger is the party identified by the kid of
the Challenge's signature.¶
A capability document MAY advertise challenge_deposit. Whether anything must accompany a Challenge, how it is posted and what becomes of it are the terms profile's and the settlement binding's to say; this document carries the member and reads it for no purpose. Section 17.14
discusses what a deposit does and does not prevent.¶
========== NOTE: '\' line wrapping per RFC 8792 ===========
{
"pact": "0.3",
"type": "Challenge",
"vtc_id": "vtc_9f2c11",
"delivery_hash": "sha256:dfabe6a5d1aaf23868dec65e1517dc0dfa911b\
41054ea10d641bb919aed34c95",
"proof": {
"profile": "acceptance",
"instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\
5c10b8351c9c55eb9316416265fc1b",
"results_hash": "sha256:7f05a60223e24ca9393e0c241d0448cb5f39e\
5edb71c117fb7d3d593d5861b40",
"results_uri": "https://watch.example/o/a91e",
"failing_checks": ["schema_valid_rate", "row_count_min"]
},
"costs": {
"amount": "1.20",
"currency": "USDC"
},
"signature": {
"protected": "...",
"signature": "..."
}
}
A contract with a pending Challenge is DISPUTED. It leaves that
state when a Verdict answers the Challenge, or when
challenge.max_dispute_seconds pass with none and the
Facilitator records dispute-lapsed. A lapsed Challenge
changes no Verdict: the Verdict that stood before it stands after it.
A Facilitator MAY accept further Challenges while DISPUTED, each of
which is pending on its own account, and MUST NOT record
window-closed until none is pending.¶
Buyer Facilitator Verifier Challenger
| | | |
| |<-- POST Verdict | |
| | [ verdict PASS ] |
| | [ window-opened ] |
| |<------------- POST Challenge ---|
| |-- Status ---------------------->|
| | [ challenge ] |
| |-- Challenge + Delivery -------->|
| |<-- POST Verdict | |
| | [ verdict FAIL, answers, |
| | supersedes ] |
| | [ children-final ] |
| | [ terminal SETTLED, |
| | challenge_upheld true ] |
|<- Outcome ---| |
The figure carries no rank, no waterfall and no amount. The -01
revision drew five numbered transfers on this diagram; every one of
them is now a line in a profile's schedule, keyed to the
terminal entry, and reported in terms_result.¶
Before a Buyer and Seller can co-sign a VTC they must agree on a Facilitator and know what it implements. This document registers one well-known URI for that purpose, per [RFC8615]. A client SHOULD fetch the document again before it proposes, since nothing in it survives the Facilitator withdrawing a profile.¶
This is deliberately narrower than agent discovery, which is the subject of separate work and is not restated here. What is discovered is one service's capabilities, not an agent's identity, skills or endpoints.¶
A Facilitator SHOULD publish a JSON document, media type
application/vnd.pact.facilitator+json, with the members in
Section 3.9, at the path /.well-known/pact-facilitator of its origin: for an https: identifier that origin, and for a did:web identifier https:// followed by the host the method encodes. The document MUST
be served over HTTPS. It MUST be signed, and the signature MUST verify
against a key bound to the identifier in facilitator. An
unsigned capability document is not usable for contract formation,
because terms_profiles determines which terms a party can name
and expect to be evaluated.¶
========== NOTE: '\' line wrapping per RFC 8792 ===========
{
"pact": "0.3",
"type": "FacilitatorCapabilities",
"facilitator": "did:web:settle.example",
"issued_at": "2026-11-01T09:00:00Z",
"settlement_bindings": [
{
"id": "https://settle.example/bindings/ledger-1",
"networks": ["eip155:8453"],
"assets": ["USDC"]
}
],
"flows": ["verdict-first", "delivery-first"],
"verification_profiles": ["acceptance"],
"terms_profiles": [
{
"id": "tag:laxsharma79@gmail.com,2026:pact:bonded-restituti\
on",
"profile_hash": "sha256:c6b85fc09b885fb082e79a2e833356628e7\
7c18a3211f81fc0accf8b07057192"
}
],
"max_contract_value": {
"amount": "50000.00",
"currency": "USDC"
},
"endpoints": {
"contract": "https://settle.example/pact/v2/contracts",
"delivery": "https://settle.example/pact/v2/deliveries",
"verdict": "https://settle.example/pact/v2/verdicts",
"challenge": "https://settle.example/pact/v2/challenges",
"outcome": "https://settle.example/pact/v2/outcomes"
},
"signature": {
"protected": "...",
"signature": "..."
}
}
A client MUST NOT infer any capability from the absence of a member. A client may hold a Facilitator\'s capability document by prior arrangement rather than fetch it from the well-known path; the path is a convenience, the document is not, since a Facilitator refuses what its document does not advertise (Section 5.3, Section 13.1). A Facilitator lists only the terms profiles whose vectors (Section 12.1) its own implementation reproduces.¶
A contract names both a tier, which says what class of
evidence is produced, and a profile, which says what is
actually done to check it. Four tier labels are used in this document:
T0-reexec, deterministic re-execution; T1-tee,
hardware attestation per [RFC9334]; T2-zkml, a
proof of inference; and T3-jury, staked arbitration. Tiers are
a vocabulary. Three profiles are defined below by name; any other is
identified by a URI under its definer's control, and this document
creates no registry for them. The distinction matters because the tier
name does not determine how much checking a contract gets and the
profile largely does.¶
Consider one task, a bulk data transformation, under two profiles at the same nominal tier. Re-executing the whole computation and comparing outputs costs about what performing it cost; running a committed acceptance instrument against the delivered artifact costs a small fraction of that. Those are the author's estimates, not measurements (the measurements Section 16 mentions are of the protocol, not the work), and the two can differ by orders of magnitude in what checking costs relative to the price. A terms profile may make that ratio matter; this document requires of a verification profile the six statements listed after the profiles below, one of which is an order-of-magnitude estimate of its cost relative to the work, since a figure nobody can estimate is a figure nobody can use.¶
Implementations SHOULD select the cheapest profile that detects the failures they actually care about, rather than the strongest-sounding one. A committed acceptance instrument that is adequate is worth more than a re-execution profile that nobody can afford to run.¶
acceptance:criteria_hash against the Delivery. The proof of nonconformance is a
failing evaluation. Deterministic by construction, since the
instrument is fixed before work begins. Cost: a small fraction of the work for a data transformation, by estimate. It evaluates nothing in constraints.¶
bisection:full-reexec:A verification profile usable with this document states six
things: what artefact is evaluated and against what; what constitutes a
valid proof of nonconformance, including whether absence of evidence
is one; that its proof can be evaluated by a party other than the
Seller; its cost relative to the work, to order of magnitude;
whether it is deterministic and with what tolerance
(Section 17.10); and whether it evaluates any part
of the TaskSpec's constraints, and if it does, which part
and how a Verdict reports the result. Under any profile a PASS speaks
to the TaskSpec's acceptance member alone; a profile that
evaluates any part of
constraints reports that result only in the results document
its results_hash commits to. Where acceptance passes
and such a profile finds a constraint violated, the Verdict is PASS
and the violation is visible only in that results document: a result
on constraints never changes outcome, and a
condition that is to decide a Verdict belongs in acceptance,
which criteria_hash commits to. The three entries above are
sketches, and a full profile document completes each; a profile
defined elsewhere states them in its own document.¶
Independence is a relation between the identifier that signs a
Verdict and three identifiers of the contract: the Buyer's, the
Seller's and the Facilitator's. It MUST be derived by the Facilitator
and MUST NOT be satisfied by a field in which a record declares itself independent. Rules of this kind are stated for evaluation after the fact in [X402COMPLIANCE]; this document binds them at contract formation. A Facilitator MUST refuse a Verdict whose signer
is, after normalization, the contract's Buyer, Seller or Facilitator,
and MUST refuse a contract whose parties.verifier is any of
those three (verifier-not-independent). The last case is the
rule the -01 revision stated as a prohibition on the Facilitator's
conduct; it is an identifier comparison and is stated as one.¶
Party identifiers MUST be normalized before comparison. Normalization decides whether two identifiers are equivalent under the rules below, and nothing more. Whether two identifiers that are not equivalent belong to one controller is outside what this protocol can see, which is why Table 4 says only that a Seller is refused as Verifier when it signs under an identifier equivalent to the Buyer's, the Seller's or the Facilitator's.¶
A party identifier, or a kid, is normalized by the
following steps, in this order. An identifier that a step finds not
evaluable is not normalized further.¶
did or
https, the two forms of Section 14.1.1; any
other scheme makes the identifier not evaluable.¶
did identifier MUST match the did rule of
[DID-CORE], which allows only lower-case letters
and digits in the method name and no path or query, so
did:WEB: does not parse. An https identifier
MUST match the URI rule of [RFC3986] with an
authority, so a character that rule does not admit, such as "\",
does not parse; it MUST NOT carry userinfo
([RFC9110], Section 4.2.4). The host of a
did:web identifier is its method-specific identifier up
to the first ":", less any port, which [DID-WEB]
writes as "%3A" and digits ("%3a" is the same escape); the host of
an https identifier is that of its authority. A port is
digits and MUST NOT begin with "0". A host MUST NOT contain "%",
MUST NOT be an IP literal, and the last of its labels that is not
empty MUST NOT begin with a digit. A segment of an https
path, or of a did:web method-specific identifier between
its ":" separators, MUST NOT be "." or "..". An https
path MUST NOT contain "//", a did:web method-specific
identifier MUST NOT contain "::", and an https query MUST
NOT be empty. An identifier that fails any of these is not
evaluable.¶
did:web or https identifier
is lower-cased. Nothing else is: the segments of a
did:web identifier after its host, and the path and query
of an https identifier, are case sensitive.¶
https host, "%3A" or "%3A443" after a did:web
host, each matched as a fixed string ([RFC3986]
Section 6.2.3). An https path that is exactly "/" is
removed.¶
Two identifiers are equivalent when their normalized forms are equal, character for character. Hosts are therefore compared as A-labels [RFC5890]: a U-label fails step 1, and a host written with escapes fails step 6.¶
Step 3 is stricter than [RFC3986], whose
Section 6.2.2.2 normalizes an escaped unreserved character by
decoding it. This document refuses that form instead, as its own
choice, so that no decoder sits on the comparison path. Step 6
refuses seven more forms rather than rewrite them: userinfo; a port
with a leading zero; a "." or ".." segment, which
[RFC3986] Section 6.2.2.3 would remove; a host with
an escape in it, which the reg-name rule of
[RFC3986] admits; a host that is an IP literal, since
one IPv6 address has several spellings, or whose last label that is
not empty begins with a digit; an empty query; and an empty segment
between two separators, "//" or "::". The rule on a host's last
label is this document's own choice, as step 3's is: it refuses
every host written as an IPv4 address in the decimal, octal or
hexadecimal forms some URL parsers accept, and some host names
besides. The refusals cover those forms and say nothing more about
aliasing. Two steps fold further than [RFC3986]
instead. Step 4 removes every "/" at the end of the identifier, so
"https://h.example/a/" and "https://h.example/a" are equivalent
here, though a server may answer them as two resources. A "/" it
removes may also end a query or a did: identifier. Step 9
removes a "." at the end of a host.¶
An identifier that is not evaluable is never compared and MUST
NOT be treated as outside the parties. A Facilitator MUST refuse a
contract that carries one in parties or
parent.facilitator, and any record signed under a
kid that is one (identifier-not-evaluable). The
refusal of a contract comes before the schema check of
Section 14.2, so a party identifier that the
published schema also rejects is refused as
identifier-not-evaluable. An independence claim reaches
exactly as far as the record's own commitments.¶
An agent that accepts work may subcontract part of it. The subcontract is an ordinary PACT contract whose Buyer is the parent's Seller. What this section adds is the binding between the two, in both directions and across Facilitators, so that a parent's Outcome Record can commit to its children's and a reader of the parent's record can find and check them.¶
A (Buyer)
|
vtc_9f2c11 at did:web:settle.example
|
B (Seller)
|
+-----------+-----------+
| |
vtc_c1a2 vtc_c2b7 at did:web:other.example
| |
C (Seller) D (Seller)
A subcontract carries parent, a top-level member with the
parent's vtc_id, vtc_hash and
facilitator. Because parent is inside the bytes both
parties sign, the child's Buyer signature is itself the authorisation
to attach that child to that parent. The -01 revision carried this
member inside the member it has since removed; it is structural and
is now where structure is.¶
The child's Facilitator need not resolve the parent, and across
Facilitators it often cannot. It MUST record parent as
signed, and MUST allow the identifier in parent.facilitator
to retrieve the child's Status and Outcome Record
(Section 17.12). The check that the child's Buyer is
the parent's Seller is made where the parent is: at registration.¶
The -01 revision required a Facilitator to reject a child whose parent chain contained the child's own identifier and to enforce a maximum depth. Neither rule survives, because neither is needed. A child commits to its parent's digest, and the parent's digest exists before the child is signed, so no contract can commit to a descendant and a cycle cannot be formed; depth is bounded by whatever a Facilitator is willing to register, and no Facilitator sees more than one level.¶
The parent's Facilitator learns of a child when the child's co-signed
contract is posted to the parent's children resource
(Section 13). Any holder of that contract may post it;
the registration is authenticated by the child's own signatures, and
the child's Buyer, which is the parent's Seller, is the party that
ordinarily holds it. A registered child is identified at the parent's
venue by its digest, so its id need not be unique there.¶
A Facilitator MUST refuse a registration, with the problem type
named, when: the body is not a valid contract (Section 14.2, the
rules on members and signatures; its terms and deadline are its own
Facilitator's to check, and this Facilitator reads the deadline only
to compute the child's latest finality instant,
Section 10.3); its parent.vtc_hash is not the
parent's digest or its parent.facilitator is not this
Facilitator (parent-unresolvable); its
parties.buyer is not the parent's parties.seller
after normalization (parent-unresolvable); its latest
finality instant is not earlier than the parent's
(Section 10.3, finality-ordering-violation);
or the parent is terminal (wrong-state). An accepted
registration is recorded as child-registered.¶
child.parties.buyer == parent.parties.seller child.parent.vtc_hash == digest(parent)¶
Without the first check any party may name any contract as its parent. The attack is cheap and asymmetric: name a competitor's contract as parent, subcontract a trivial task to yourself, fail it, and put a failed child under the competitor's record. The -00 revision carried the parent as a bare string with no hash and no check, so the attack cost one signature.¶
A child becomes final for its parent when the parent's Facilitator
holds the child's Outcome Record. It may obtain that record itself,
by retrieving it from the child's Facilitator, or receive it by a POST to the child's entry under that resource
(Section 13). Either way the Facilitator MUST verify
the record's Facilitator signature against a key bound to the
identifier the registration recorded, and MUST verify that its vtc_hash is the registered child's digest, refusing a record that fails either check (child-outcome-invalid), before recording child-final.
The Facilitator MUST also refuse, as child-outcome-invalid,
a record whose parties, task.spec_hash,
verification or flow is not the registered child
contract's, or whose outcome.verdict is not the value its
own trace gives (Section 12). The parent's Facilitator
does not hold the child's Delivery, so it cannot check that a
record's work_hash is that Delivery's.
The Facilitator MUST refuse a record that differs from one it already
holds for that child (object-conflict), and, once the parent
has its terminal entry, a record for any child whose record
the Facilitator does not hold (wrong-state). Where the child's latest finality instant passes
with no record held, the Facilitator records
child-unresolved. children-final follows when every
registered child has a child-final or a
child-unresolved entry, or both, and the parent's
terminal entry follows that.¶
A child that is never registered does not exist for the parent. Nothing in this document compels a parent's Seller to register a child, and Section 17.6 says what that means.¶
A parent's Outcome Record MUST carry
children_merkle_root over the Outcome Records of its
registered children (Section 12.2), so a parent cannot be
recorded until its children have been, and the parent waits in
AWAITING_CHILDREN until they are. For that wait to be bounded, every
child must be able to reach a terminal state, or be declared
unresolved, before its parent needs it.¶
The latest finality instant L of a contract is computed from its own members and nothing else:¶
verdict-first: L = task.deadline
+ verification.max_verdict_seconds
+ challenge.window_seconds
+ challenge.max_dispute_seconds
delivery-first: L = task.deadline
+ challenge.window_seconds
+ challenge.max_dispute_seconds
no-window: L = task.deadline
¶
Every wait in Table 2 is bounded by one of
those members, and a Challenge can only be received before
closes_at, so no sequence of events carries a contract past
its L except waiting for its own children, and a
verdict-lapsed entry recorded after the instant it fell
due, whose window then closes that much later. A Facilitator MUST refuse
to register a child unless L(child) is earlier than L(parent), and
MUST record child-unresolved for a registered child no later
than the first opportunity after L(child) if it holds no Outcome
Record for it by then.¶
The -01 revision compared the child's latest finality with the
parent's earliest window close, and bounded neither: under its
default mode the first Verdict could take forever, so the inequality
guaranteed nothing. max_verdict_seconds is what makes L
finite, and the waiting state is what makes the rule honest about
the case where a child is late anyway.¶
parent |== work ==|= verdict =|= window =|= dispute =|
^ L(parent)
child |== work ==|= vrd =|= win =|= dsp =|
^ L(child)
a child is registered only where L(child) < L(parent)
What a child's outcome means for its parent is not stated here. No entry in a parent's schedule depends on any child's outcome unless the named terms profile says so; what this document guarantees is that the parent's Outcome Record commits to whichever child records exist when it is issued and names, in its trace, every child that does not.¶
A Contract Status is a JSON object, media type
application/vnd.pact.status+json, with the members in
Section 3.7, signed once by the Facilitator. It is the
body of every successful response to a POST in
Section 13 and of a GET on a contract resource. It
carries the contract's state and the trace recorded so far.¶
========== NOTE: '\' line wrapping per RFC 8792 ===========
{
"pact": "0.3",
"type": "ContractStatus",
"vtc_id": "vtc_9f2c11",
"vtc_hash": "sha256:2db525ce98c8ee4b4cb63785450439487513ef2a2d9\
5a87de407e2f4717a53f0",
"state": "WINDOW_OPEN",
"trace": [
{
"event": "accepted",
"at": "2026-11-01T10:00:00Z",
"object": "sha256:2db525ce98c8ee4b4cb63785450439487513ef2a2\
d95a87de407e2f4717a53f0"
},
{
"event": "funded",
"at": "2026-11-01T10:00:00Z"
},
{
"event": "delivered",
"at": "2026-11-10T08:30:12Z",
"object": "sha256:dfabe6a5d1aaf23868dec65e1517dc0dfa911b410\
54ea10d641bb919aed34c95"
},
{
"event": "verdict",
"at": "2026-11-10T09:14:30Z",
"object": "sha256:12d21e99864c7c85c517fc5cd51b49eb0a3ca281e\
f8f885ac6451eb1788ab085",
"signer": "did:web:audit.example#k1",
"outcome": "PASS"
},
{
"event": "window-opened",
"at": "2026-11-10T09:14:30Z",
"closes_at": "2026-11-10T10:14:30Z"
}
],
"issued_at": "2026-11-10T09:14:30Z",
"signature": {
"protected": "...",
"signature": "..."
}
}
Two rules make a Status worth keeping. A Facilitator MUST issue a Status for every POST it accepts, carrying the entry that request caused, so that the requester holds a signed receipt of what was recorded and when. And the trace in every Status a Facilitator issues for a contract MUST be a prefix of the trace in every later one; two Statuses for one contract that violate that are evidence of equivocation, and Section 17.1 says what a holder can do with it. The Outcome Record's trace is the last such sequence.¶
A Status is evidence of the contract's state and trace as the
Facilitator that signed it held them at its issued_at, and
of nothing after that instant. It cannot still be
current after the next instant at which the Facilitator's clock alone
changes the state, which its trace, the contract and each registered
child's contract give:
task.deadline before delivered; delivered
plus verification.max_verdict_seconds under
verdict-first while no Verdict is recorded;
closes_at while the state is WINDOW_OPEN; a challenge
entry plus challenge.max_dispute_seconds while that
Challenge is pending; and a child's latest finality instant
(Section 10.3). A record posted, a report from the
settlement binding, or a child's Outcome Record the Facilitator
obtains itself can make it stale before that instant, and nothing in
a Status says whether one did. This document defines no freshness
member: a holder that needs the current state asks again, at the
contract resource of Section 13, which the
endpoints member of the capability document names
(Section 8), and is answered only where
Section 17.12 lets it retrieve that resource: by
default a party to the contract, the parent's Facilitator, or a
requester the Facilitator has chosen to admit. A holder it does not
answer has only the copy a party handed it.¶
The -01 revision returned the posted object with a state
member added to it, which no schema admitted and no signature covered.
The Status replaces that: the posted object is not echoed, and
everything in the response is inside the Facilitator's signature.¶
An Outcome Record records what a contract did. It is a JSON object,
media type application/vnd.pact.outcome+json, with the members
in Section 3.8. It is the input to any reputation
system built on PACT, though this document defines no such system and
takes no position on how the records should be weighed.¶
A Facilitator MUST issue exactly one Outcome Record for every contract that reaches a terminal state, including SETTLED and ABANDONED, MUST sign it, and MUST NOT require the signature of any other party on it. A Facilitator MUST serve the bytes of the record it signed rather than sign it again on retrieval; under a randomized signature scheme a second signing would produce a second record with a different digest. The -00 revision's record needed the signature of the party it recorded against, which made a reputation layer built on it structurally incapable of recording a loss. The Facilitator signature is what makes the record evidence: without it the record is a claim by interested parties about themselves, and with it a fabricated history requires a Facilitator's key rather than two identities.¶
The record also says what was checked, and how the contract ended,
without a fetch of the contract. A Facilitator MUST copy
task.spec_hash, verification and flow from
the contract, as it copies parties; vtc_hash already
commits to all of them. A Facilitator MUST set
outcome.verdict to the digest of the Verdict standing at the
terminal entry (Section 4.2); where none stands,
to verdict-lapsed when the trace has a
verdict-lapsed entry, and to none otherwise. A
verified PASS, a Verdict that never came and a flow that accepts none
therefore end in three different records, though all three end in
FINAL. A delivery-first contract that ended FINAL with no
Verdict recorded carries none as a no-window
contract does, since verdict-lapsed is recorded only under
verdict-first, so outcome.verdict is read together
with flow, which says whether a Verdict could have been
recorded. A Facilitator MUST copy work_hash from the
Delivery whose digest the delivered entry carries, and MUST
NOT include it where the trace has no delivered entry. Each
of these members follows from the contract, the trace and the
Delivery, so any holder of those can check it.¶
========== NOTE: '\' line wrapping per RFC 8792 ===========
{
"pact": "0.3",
"type": "OutcomeRecord",
"vtc_id": "vtc_9f2c11",
"vtc_hash": "sha256:2db525ce98c8ee4b4cb63785450439487513ef2a2d9\
5a87de407e2f4717a53f0",
"parties": {
"buyer": "did:web:buyer.example:agents:procure-1",
"seller": "did:web:dataforge.example:agents:etl-3",
"facilitator": "did:web:settle.example",
"verifier": "did:web:audit.example"
},
"task": {
"spec_hash": "sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef11\
1d27ff6bee37f05531823b72"
},
"verification": {
"tier": "T0-reexec",
"profile": "acceptance",
"criteria_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\
10b8351c9c55eb9316416265fc1b",
"max_verdict_seconds": 86400
},
"flow": "verdict-first",
"outcome": {
"state": "SETTLED",
"challenge_upheld": true,
"verdict": "sha256:b8afa3db5aac3af7c1414d26a397dc7a62b9037433\
9150f36bf9a27b506d0ef8"
},
"work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee2\
80a3170b6b9a5037036ef0",
"trace": [
{
"event": "accepted",
"at": "2026-11-01T10:00:00Z",
"object": "sha256:2db525ce98c8ee4b4cb63785450439487513ef2a2\
d95a87de407e2f4717a53f0"
},
{
"event": "funded",
"at": "2026-11-01T10:00:00Z"
},
{
"event": "delivered",
"at": "2026-11-10T08:30:12Z",
"object": "sha256:dfabe6a5d1aaf23868dec65e1517dc0dfa911b410\
54ea10d641bb919aed34c95"
},
{
"event": "verdict",
"at": "2026-11-10T09:14:30Z",
"object": "sha256:12d21e99864c7c85c517fc5cd51b49eb0a3ca281e\
f8f885ac6451eb1788ab085",
"signer": "did:web:audit.example#k1",
"outcome": "PASS"
},
{
"event": "window-opened",
"at": "2026-11-10T09:14:30Z",
"closes_at": "2026-11-10T10:14:30Z"
},
{
"event": "challenge",
"at": "2026-11-10T09:40:00Z",
"object": "sha256:298494c02feb0a51def024b5e22acf11aa65d0f4e\
175271f07962215f26e71a9",
"signer": "did:web:watch.example#k1",
"costs": {
"amount": "1.20",
"currency": "USDC"
}
},
{
"event": "verdict",
"at": "2026-11-10T09:58:05Z",
"object": "sha256:b8afa3db5aac3af7c1414d26a397dc7a62b903743\
39150f36bf9a27b506d0ef8",
"signer": "did:web:audit.example#k1",
"outcome": "FAIL",
"answers": "sha256:298494c02feb0a51def024b5e22acf11aa65d0f4\
e175271f07962215f26e71a9",
"supersedes": "sha256:12d21e99864c7c85c517fc5cd51b49eb0a3ca\
281ef8f885ac6451eb1788ab085"
},
{
"event": "children-final",
"at": "2026-11-10T09:58:05Z"
},
{
"event": "terminal",
"at": "2026-11-10T09:58:05Z",
"state": "SETTLED",
"challenge_upheld": true
}
],
"terms_result": {
"profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restit\
ution",
"profile_hash": "sha256:c6b85fc09b885fb082e79a2e833356628e77c\
18a3211f81fc0accf8b07057192",
"currency": "USDC",
"transfers": [
{
"event": 8,
"from": "...",
"to": "...",
"amount": "...",
"code": "..."
}
]
},
"signatures": [
{
"protected": "...",
"signature": "..."
}
]
}
The record carries one signature, the Facilitator's. The Seller did
not consent to this record and its consent is not required. The
transfers entries are elided here because their content is the
profile's; Appendix A shows them filled in for
its own profile.¶
terms_result reports what the named profile's schedule
produced over the whole trace. It carries the profile identifier and
hash copied from the contract, the currency, and transfers:
an array of entries, in the order the schedule produced them, each
with event (the zero-based index of the trace entry at which
the schedule emitted it), from and to (account
names as the profile defines them), amount (an amount of
Section 2, written with the fewest decimal places
that keep its value and never fewer than two, so that two
Facilitators that produce one list sign the same bytes), and
code (a string the profile defines, naming the schedule
line that produced the entry).¶
This document defines the form of the list and two arithmetic
facts about it, and nothing about what any entry means. Over the internal accounts the profile declares (Section 5.3), which open empty: no entry takes from an account more than
that account holds at that point in the list; and after the last
entry every account the profile marks internal holds zero. The two facts are constraints on a profile, checked against its vectors before a Facilitator lists it (Section 8); a Facilitator MUST NOT sign an Outcome Record whose list breaks either, since such a list shows the profile it evaluates to be defective, and MUST NOT sign one whose list differs from what the profile's schedule produces for the record's own trace. A Facilitator that finds a list breaking either records nothing of the request that produced it and answers internal-error. Any party
holding the contract, the trace and the profile's bundle can
recompute the list; that is the property the experiment in
Section 1.4 depends on.¶
vectors.json in a profile's bundle is an array of
objects, each with name, contract (a VTC, or the
members of one the schedule reads), and then either trace (a complete
trace) with transfers (the list the schedule produces for
it) and accounts (the profile's internal accounts, which open empty and must close empty), or admission (an object carrying either admitted, true, or refused, the problem type the admission rule answers with for that contract). A Facilitator MUST reproduce every vector of a profile before
listing that profile in its capability document
(Section 8), which is the only conformance
requirement this document places on a profile implementation.¶
Let D be the list of 32-byte SHA-256 digests of the Outcome Record
of each registered child for which the Facilitator holds one, each
computed over the record's canonical form including its
signatures member, sorted ascending as byte strings.
children_merkle_root is MTH(D) exactly as defined in
[RFC9162] Section 2.1.1, with SHA-256 as the hash: a
leaf is SHA-256(0x00 || d), an interior node is SHA-256(0x01 || left
|| right), and for n greater than one the list is split at k, the
largest power of two smaller than n. The shape is therefore fixed by
n alone, and two implementations that agree on D agree on the
root.¶
The domain separation is not optional, because the prefixes are what make MTH the function [RFC9162] defines, and a second implementation must compute the same root. The second-preimage attack the prefixes guard against, a leaf input chosen to equal an interior node's input, needs a leaf of that input's length; the fixed 32-byte digests in D cannot supply one, so here the prefixes buy agreement with the RFC rather than a defence the construction would otherwise lack.¶
The member is present when at least one child is registered and
absent otherwise; it MUST NOT be present with an empty or zero value,
which would be indistinguishable from a tree whose children were
withheld. Where the Facilitator holds no child's record D is empty
and the root is MTH of the empty list, SHA-256 of the empty string.
A child whose child-unresolved entry no
child-final entry follows is one whose record the root does
not cover. The -01 revision did not say whether a leaf covered the record's signatures; this revision says it does, so a record cannot be re-signed without changing the root.¶
This section specifies the operations a Facilitator exposes. This document fixes no base URI; each is discovered from the endpoints member of the capability document
(Section 8), so a Facilitator may mount them anywhere
on its origin.¶
{contract}; body, a
contract; 201 with a Status.¶
{contract}/{id}; 200 with a Status.¶
{contract}/{id}/children; body, the child's contract; 201
with a Status.¶
{contract}/{id}/children/{child_hash}, where child_hash is the digest by which the registration identifies the child; body, the child's Outcome Record; 200 with a Status. This resource is keyed by digest, so the id rule of Section 13.2 does not apply to it.¶
{delivery}; body, a
Delivery; 202 with a Status.¶
{verdict}; body, a
Verdict; 201 with a Status.¶
{challenge}; body, a
Challenge; 202 with a Status.¶
{outcome}/{id}; 200 with the Outcome Record.¶
In these templates {id} is the contract's id
percent-encoded as a single path segment.¶
A request naming a contract the Facilitator does not hold is refused
as unknown-contract (404). A body larger than the Facilitator
accepts is refused as payload-too-large (413). A failure inside
the Facilitator is reported as internal-error (500), the one
type in Section 19.3 that names no rule. A GET of an
Outcome Record before the
terminal entry is refused as wrong-state.¶
All requests and responses use the media types defined in
Section 19. All requests MUST be made over HTTPS, following
the recommendations of [RFC9325]. Status codes are as
defined in [RFC9110]. A Delivery and a Challenge are answered 202 (Accepted) because, in the sense of [RFC9110] Section 15.3.3, their processing is not complete when the response is sent: what either record leads to may depend on a Verdict the Facilitator does not itself produce. A contract and a Verdict are answered 201 (Created); the contract is the resource the Location header names, and a Verdict, to which this document gives no resource of its own, is identified by the digest the Status in the response carries.¶
A Facilitator authenticates the sender of a POST by the signature on the body, and by nothing else in this document: it MUST reject a Delivery not signed by the contract's Seller, a Verdict not signed by a party admissible under Section 7.2, a Challenge whose signer it cannot resolve, and a child registration or child outcome whose body does not verify as Section 10.2 requires. A Facilitator MAY require an HTTP-layer authentication in addition. Retrieval is discussed in Section 17.12.¶
The request body is a VTC carrying the signatures of both parties
required to sign it. A Facilitator MUST perform the checks in
Section 14, Section 5.3 and
Section 9.1 before creating the resource, MUST
refuse a contract whose parties.facilitator is not itself or
whose price.settlement, network or currency it does not
advertise (facilitator-mismatch,
settlement-unsupported), and MUST refuse otherwise with the
problem type that names the rule. settlement-unsupported also covers a price stated in a currency other than max_contract_value's and a price above it; a verification profile the Facilitator does not list is refused as settlement-unsupported.¶
Every object this protocol carries is committed by the digest of
its own canonical form, so no separate idempotency key is needed and
none is defined; the general mechanism of
[I-D.ietf-httpapi-idempotency-key-header] solves a
problem this protocol does not have. A Facilitator MUST treat a POST
whose body has a digest it has already accepted as a request for the
existing resource, and MUST respond 200 (OK) with the
current Status rather than creating a second resource or reporting a
conflict.¶
Where a POST carries the same object id as an existing
resource but a different digest, the Facilitator MUST respond
409 (Conflict) (object-conflict). Retrying a submission is therefore safe when the same bytes are resent, and altering one never is. A record signed afresh is a different record with a different digest, not a retry: ECDSA signatures are randomized unless produced as [RFC6979] describes, so a client signing with ES256 or ES384 SHOULD sign deterministically or keep the bytes it sent and resend those.¶
A Facilitator MUST report failures using
[RFC9457] problem details, media type
application/problem+json, with a type from
Section 19.3 for a rule in this document, or from
the profile's own namespace for a rule in a terms profile. A problem
arising from a rule in this document MUST carry section, the
number of the section stating the rule. A problem arising from a
rule in a terms profile MUST carry profile and
profile_section instead, since section cannot name
a rule outside this document. Error responses name the rule that was
violated, because a conformance failure a caller cannot locate is a
failure of the specification.¶
Buyer/Seller Facilitator Verifier
| | |
|-- POST {contract} --->| |
|<-- 201 Status --------| |
| | |
|-- POST {delivery} --->| |
|<-- 202 Status --------| |
| | |
| |-- GET work_uri ----->|
| |<-- POST {verdict} ---|
| |-- 201 Status ------->|
| | |
|-- GET {contract}/id ->| |
|<-- 200 Status --------| |
| | |
|-- GET {outcome}/id -->| |
|<-- 200 Outcome -------| |
Every rule a PACT conformance checker enforces is stated in this document as normative text. This section collects the rules that a schema language cannot express, so that an implementation built from this document alone passes a conformance suite built from it. A rule that lives only in a test suite is not a requirement, and an implementer who cannot find it in the specification will not implement it.¶
Every signature carried by a VTC, Delivery, Verdict, Challenge, Status, Outcome Record or capability document has, for each signer, the form of one signature object of the JWS [RFC7515] General JSON Serialization, Section 7.2.1 of that document, with the payload detached as its Appendix F describes. The payload is BASE64URL of the JCS-canonical bytes of the object with the signing member removed, so the JWS Signing Input is ASCII(BASE64URL(UTF8(protected)) || "." || BASE64URL(JCS(object))) exactly as Section 5.1 of [RFC7515] defines it. The payload is never transmitted; a verifier reconstructs it from the object it holds, and verifies over the protected header exactly as transmitted, never over a header it re-serialized. The following constraints apply.¶
alg, kid and
typ.¶
alg MUST be Ed25519 [RFC9864] with an Ed25519 key, or ES256 or ES384 [RFC7518] with a P-256 or P-384 key. A verifier MUST reject any other value, including the polymorphic EdDSA identifier of [RFC8037] that [RFC9864] deprecates, and MUST reject none (algorithm-not-permitted). Absent
an allowlist an attacker selects the algorithm, which permits both
unsigned acceptance and confusion of a public key for a symmetric
secret.¶
kid MUST appear inside the protected header and MUST
NOT be carried as a sibling of it. A key identifier outside the
signed bytes is rewritable in transit, which allows an attacker who
can publish a key document to re-attribute a genuine signature to
itself.¶
jwk, jku,
x5c, x5u, x5t, x5t#S256 or
crit, and a signature entry MUST NOT carry an unprotected
header; a verifier MUST reject an entry carrying any of them
(signature-invalid). A key travels by reference and never
inline, so that the kid rule cannot be bypassed.¶
alg
requires: Ed25519 for Ed25519, P-256 for
ES256, P-384 for ES384. A mismatch is
signature-invalid.¶
typ MUST be the full media type of the object signed,
including the application/ prefix, so that a signature
over one object type cannot be replayed as a signature over
another. Section 4.1.9 of [RFC7515] recommends
omitting the prefix; this document requires the full form so that
typ equals the registered media type character for
character. Explicit typing follows Section 3.11 of
[RFC8725].¶
signatures array MUST be sorted by the normalized
kid of its entries (Section 9.1), ties
broken by the unnormalized kid, both compared as
sequences of Unicode code points; a verifier MUST reject an
unsorted array (signatures-unordered). Two clients that
each attach their own entry and exchange the object would
otherwise produce two arrays, and since the digest covers the
array, two digests for one agreement.¶
s value in the low
half of the curve order, that is s at most n/2 for the
order n of the curve [SP800-186], and a verifier
MUST reject one that does not. [RFC7518] fixes the
encoding and not which of the two valid s values is
accepted; accepting both lets anyone holding a valid signature
produce a second one over the same bytes without the key, and a
second signature is a second digest. Ed25519 verification per
[RFC8032] already rejects a non-canonical
S, so the rule is stated for ECDSA only.¶
A kid is a URI naming a public key. A verifier MUST
resolve it as follows, and MUST reject a signature whose
kid it cannot resolve.¶
did: identifier is a DID
[DID-CORE] followed by a fragment: a DID URL with
no path and no query once step 4 of
Section 9.1 has removed every "/" at its end,
since step 6 refuses both. The verifier
resolves the DID document
by the method the identifier names and selects the verification
method its fragment identifies. Examples in this document use
did:web [DID-WEB]; no method is
required or excluded.¶
https: identifier dereferences, over TLS, to a
JWK Set [RFC7517]; the verifier selects the key
whose kid member equals the fragment.¶
The part of a kid before its fragment MUST equal, after
the normalization in Section 9.1, the party
identifier the signature is attributed to. Verifying a signature
establishes that the holder of that key signed; that the key
belongs to the party is a property of the identity method, and this
document does not add to it. An identity system for agents defined
elsewhere, such as [I-D.ietf-wimse-aims], is used by naming its identifiers here in one of these two forms; a further form needs a resolution rule added to this list, which is the one change it would take.¶
parties.buyer and parties.seller MUST be
distinct after the normalization in
Section 9.1 (parties-not-distinct).¶
kid covers parties.buyer, exactly one whose
kid covers parties.seller, and no other
(signature-missing, unexpected-signer). A count
of signatures is not sufficient: two signatures covering one
identifier MUST be rejected.¶
challenge.window_seconds, challenge.max_dispute_seconds and verification.max_verdict_seconds MUST be greater than zero, and task.deadline MUST be later than the instant of
acceptance (deadline-invalid). A contract whose latest
finality instant (Section 10.3) falls after the
year 9999 MUST be refused at acceptance the same way, since a
closes_at its trace could carry would then have no
timestamp form (Section 2).¶
harness_uri, rubric_uri, schema_uri or
sample_uri without its hash.¶
acceptance object MUST carry the members required
for the contract's tier. An empty acceptance object MUST be
rejected.¶
terms.profile and terms.profile_hash MUST
match an entry the Facilitator advertises, and
terms.parameters MUST validate against that profile's
schema (terms-unsupported,
terms-parameters-invalid).¶
schema-invalid), which includes the form of
every amount (Section 2). An amount in a posted
contract or Challenge, price.amount or
costs.amount, carrying more decimal places than the
settlement binding named in price.settlement supports is
refused (amount-invalid).¶
Most rules above have an accepting and a rejecting form; the table carries the ones a suite most often gets wrong. A conformance suite built from this section alone, with no reference to any implementation, should reach the same verdicts. Rejecting vectors name the rule they violate.¶
| ID | Mutation from a valid object | Expect |
|---|---|---|
| V-01 | unmodified valid VTC | accept |
| V-02 |
alg set to none
|
reject |
| V-03 |
alg set to HS256
|
reject |
| V-04 |
kid moved outside the protected
header |
reject |
| V-05 |
typ of a Delivery on a VTC
signature |
reject |
| V-06 | buyer and seller set to the same identifier | reject |
| V-07 | buyer and seller differing only by trailing "/" | reject |
| V-08 | two signatures, both from the buyer | reject |
| V-09 |
window_seconds of 0 |
reject |
| V-10 |
acceptance as an empty object |
reject |
| V-11 |
harness_uri with
harness_hash removed |
reject |
| V-12 |
terms.profile_hash not advertised
by the Facilitator |
reject |
| V-13 |
terms.parameters failing the
profile's schema |
reject |
| V-14 | Delivery with evidence absent |
reject, no entry |
| V-15 | child whose buyer is not the parent's seller | reject |
| V-16 | child with L(child) not earlier than L(parent) | reject |
| V-17 | Verdict signed by the seller | reject |
| V-18 | object keys ordered by code point rather than UTF-16 unit, a supplementary-plane key beside one in U+E000 to U+FFFF | digest mismatch |
| V-19 | buyer and seller differing only in the
case of a did:web path |
accept |
| V-20 | object carrying a member this document does not define for it | reject |
| V-21 |
signatures not sorted by
normalized kid
|
reject |
| V-22 | ECDSA signature with s above
n/2 |
reject |
| V-23 | Verdict with delivery_hash computed
over the Delivery without its signature |
reject |
| V-24 | Outcome Record whose transfers
overdraw an account of the profile |
reject |
| V-25 | a number serialized by the host language's
default formatter, such as 1.0 for the float
one |
digest mismatch |
| V-26 | a protected header carrying a member Section 14.1 forbids: jwk, jku, x5c, x5u, x5t, x5t#S256 or crit
|
reject |
| V-27 |
parties.verifier differing from
parties.seller only in the case of the host |
reject |
| V-28 |
parties.verifier differing from
parties.seller only by a "." at the end of a
did:web host that a path follows |
reject |
| V-29 |
parties.verifier differing from an
https parties.seller only by an explicit
":443" |
reject |
| V-30 |
parties.verifier differing from
parties.seller only by "%3A443" after the
did:web host |
reject |
| V-31 |
parties.verifier equal to
parties.seller with one unreserved character
percent-encoded, such as "%41" for "A" |
reject, not evaluable |
| V-32 |
parties.verifier equal to
parties.seller with one character of a path segment
percent-encoded twice, such as "%2541" for "A" |
accept |
| V-33 | a party identifier whose host is a U-label | reject, not evaluable |
| V-34 |
parties.verifier equal to
parties.seller followed by a space and a
fragment |
reject, not evaluable |
| V-35 |
parties.verifier equal to
parties.seller with the DID method name
upper-cased |
reject, not evaluable |
| V-36 | a Verdict, on a contract naming no
verifier, signed under a kid that does not
parse |
reject, not evaluable |
| V-37 |
parties.verifier equal to an
https parties.seller with "\" in place of
a "/" |
reject, not evaluable |
| V-38 |
parties.verifier equal to an
https parties.seller with userinfo
added |
reject, not evaluable |
| V-39 |
parties.verifier differing from an
https parties.seller only by a port
written "0443" |
reject, not evaluable |
| V-40 |
parties.verifier equal to
parties.seller with a "." or ".." segment
added |
reject, not evaluable |
| V-41 | a contract whose Seller signature is under
a kid that differs from parties.seller only
by a "." at the end of a path segment after the host |
reject |
| V-42 |
parties.verifier equal to
parties.seller with a "%" added that does not begin
an escape |
reject, not evaluable |
| V-43 |
parties.verifier differing from an
https parties.seller only by a path of "/"
before the query the two share |
reject |
| V-44 | Outcome Record whose work_hash is not
that of the Delivery its delivered entry
names |
reject |
| V-45 | Outcome Record of a contract that ended FINAL
on a standing PASS, with outcome.verdict other than
that Verdict's digest |
reject |
| V-46 | Outcome Record of a contract that ended FINAL
after verdict-lapsed with no Verdict, with
outcome.verdict other than
verdict-lapsed
|
reject |
| V-47 | Outcome Record of a no-window
contract, with outcome.verdict other than
none
|
reject |
| V-48 | Delivery whose
evidence.instrument_hash is not
verification.criteria_hash
|
reject, no entry |
| V-49 | Challenge whose costs.amount carries
more decimal places than the settlement binding
supports |
reject |
| V-50 | contract whose
challenge.window_seconds puts its latest finality
instant after the year 9999 |
reject |
| V-51 | trace entry omitting a member
Table 2 requires for its event: a
window-opened entry without
closes_at
|
reject |
| V-52 | trace entry carrying a member
Table 2 does not list for its event:
closes_at on an accepted
entry |
reject |
| V-53 |
schema_uri, sample_uri or
rubric_uri with its sibling hash removed |
reject |
| V-54 | a child's Outcome Record from another venue, signed by the Facilitator its registration recorded, posted to the parent's Facilitator | accept |
| V-55 | a child's Outcome Record signed by a Facilitator other than the one its registration recorded | reject |
| V-56 | contract whose id is "." or
".." |
reject |
| V-57 |
parties.verifier whose host is an
IP literal, such as "[2001:db8::1]" |
reject, not evaluable |
| V-58 |
parties.verifier whose host is an
IPv4 address, such as "192.000.002.001" or
"0xc0.0.2.1" |
reject, not evaluable |
| V-59 |
parties.verifier equal to an
https parties.seller with an empty query
added |
reject, not evaluable |
| V-60 |
parties.verifier equal to
parties.seller with an empty segment added between
two of its segments, "a//b" or "a::b" for "a/b" or
"a:b" |
reject, not evaluable |
| V-61 | a child's Outcome Record, signed by the
Facilitator its registration recorded, whose
verification is not the registered child
contract's |
reject |
| V-62 | Outcome Record of a
delivery-first contract that ended FINAL with no
Verdict recorded, with outcome.verdict other than
none
|
reject |
V-07 and V-18 are the two most often got wrong. V-07 fails wherever
party comparison is a string equality on unnormalized identifiers.
V-18 fails wherever canonicalization sorts keys by Unicode code point,
which agrees with the required UTF-16 order for every ASCII key and so
passes every vector an implementer would think to write. V-19 is the
opposite mistake, folding more than Section 9.1
allows, and the -01 reference validator made it. V-25 is the number
half of the V-18 mistake: [RFC8785] prints numbers as
ECMAScript does, so the float one is 1 and never
1.0. The -02 reference canonicalizer printed 1.0
until this vector caught it, and the spec_hash, vtc_hash and delivery_hash of Section 15 changed when it was fixed.¶
A vector whose expected result reads "not evaluable" passes only
with the problem type identifier-not-evaluable
(Section 9.1). Most of those identifiers are also
rejected by an implementation that rewrites them where
Section 9.1 refuses them, but as
verifier-not-independent, and that implementation fails
the vector.¶
V-31 and V-32 are a pair. An implementation that compares an escaped unreserved character as written fails V-31, and so does one that decodes it once, as [RFC3986] Section 6.2.2.2 would. One that decodes more than once fails V-32. V-34 fails wherever whitespace is stripped before the fragment is removed, which is the order the -02 revision gave. V-41 fails wherever a "." is removed from the end of a path segment, which the -02 text did. V-42 fails wherever a "%" that begins no escape is compared as written, which the -02 reference implementation did, and V-43 wherever a path of "/" is kept before a query, which the -02 text did.¶
V-44 to V-47 and V-62 are checks any holder of an Outcome Record
can make: V-45 to V-47 and V-62 against the record's own trace, V-44
against the Delivery as well. V-45 to V-47 are the three contracts
that the -02 revision ended in the same FINAL. V-62 is a fourth
FINAL, a delivery-first contract with no Verdict recorded,
whose record carries none as a no-window record
does and differs from it in flow.¶
The digests below are the reference repository's, at the
tag named in Section 16. The object figures in earlier
sections are the repository's objects with their signatures
abbreviated and the contract's parameters elided; the digests
here are computed over the full objects. The figures that the -01 revision printed here
about a bond and a required detection rate are now the profile's; Appendix A carries the rule and the parameters.¶
A buyer commissions a data transformation at a price of 180.00 USDC
under the verdict-first flow, the acceptance
verification profile, and the terms profile of
Appendix A with the parameters shown there. The
digests carried by the reference TaskSpec, contract and profile
are:¶
========== NOTE: '\' line wrapping per RFC 8792 =========== spec_hash sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef111\ d27ff6bee37f05531823b72 criteria_hash sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b83\ 51c9c55eb9316416265fc1b profile_hash sha256:c6b85fc09b885fb082e79a2e833356628e77c18a3\ 211f81fc0accf8b07057192 vtc_hash sha256:2db525ce98c8ee4b4cb63785450439487513ef2a2\ d95a87de407e2f4717a53f0 delivery_hash sha256:dfabe6a5d1aaf23868dec65e1517dc0dfa911b410\ 54ea10d641bb919aed34c95¶
criteria_hash is the manifest digest of
Section 5.1 over the acceptance instrument bundle, and the
same value appears as acceptance.harness_hash inside the
TaskSpec, so the instrument is committed both by the contract and from
within the specification it belongs to. profile_hash is the
same construction over the profile bundle. vtc_hash is the
digest of the signed contract, and delivery_hash of the signed
Delivery, both per Section 2.¶
The -01 revision printed three of the values above, spec_hash, criteria_hash and vtc_hash, and each differs from what it printed, for reasons recorded so that a reader comparing the two documents can account for the difference. spec_hash and vtc_hash differ because the -01 canonicalizer serialized numbers as the host language printed them (Section 14.3, V-25); spec_hash also because the TaskSpec now carries the sibling hashes Section 5.1 always required, and vtc_hash also because the contract's members changed ([I-D.laxsharma-pact-02], Appendix B). criteria_hash carries no number; it differs because the two files of the instrument bundle were edited to drop their mention of the withdrawn call-for-bids example. profile_hash is new, and delivery_hash, which the -01 figures showed only as a placeholder, now covers the Delivery's signature.¶
Against the -02 revision, spec_hash and
criteria_hash are unchanged. profile_hash differs
because the profile bundle changed (Appendix B),
vtc_hash because the contract carries that
profile_hash and the pact version 0.3, and
delivery_hash because the Delivery carries that
vtc_hash and the same version.¶
The traces the reference implementation records for this contract on the path of Figure 1 and on the dispute path of Figure 7 carry the event sequences of the first two vectors in the profile's bundle, and the transfer lists the profile produces for them are those vectors' lists; the vectors name their objects by placeholder digests, so the match is of sequence and lists, not of bytes. Appendix A prints both lists.¶
This section records the status of known implementations of this document per [RFC7942]. The section and the reference to [RFC7942] are to be removed before publication as an RFC, and the listing of an implementation here implies no endorsement by the IETF.¶
One implementation is known to the author, and the author wrote it: https://github.com/pact-spec/spec, under the Apache License 2.0. At tag v0.3.0 it comprises the object schemas, the examples whose digests Section 15 prints, a conformance validator that runs 190 checks including every vector of Section 14.3, a Facilitator serving the endpoints of Section 13 with the profile of Appendix A, and clients for the other roles. Its earlier tags, v0.1.0 and v0.2.0, implemented the -01 and -02 revisions, and v0.1.0 is the source of the measurements the author has published about it. No second implementation exists, so nothing in Section 1.4 has been tested, and this document claims no interoperability. It is an individual submission and the product of no working group; the implementation is a prototype, the information is current as of the tag named above, and the contact is the author.¶
Most of what follows was found by adversarial review of earlier revisions rather than anticipated when they were written. Each subsection states the attack, why it worked, and the requirement in this document that closes it. Where a threat is only mitigated rather than closed, that is said. The table first: for each party, what the protocol enforces against it, what it records about it, and who can check the record without trusting the Facilitator.¶
| Party | Enforced against it | Recorded about it | Checkable by |
|---|---|---|---|
| Buyer | cannot alter the task, instrument or terms after signing; cannot attach a child to a contract it is not party to | its signature on the contract; any Challenge it signs | anyone holding the contract |
| Seller | cannot deliver against a substituted instrument or input; is refused as Verifier when it signs under an identifier equivalent to the Buyer's, the Seller's or the Facilitator's; cannot re-sign a record without changing every digest over it | its signature on the contract and the Delivery; the Verdicts and Challenges on its Delivery | anyone holding the contract and the Delivery |
| Verifier | is refused when it signs under an identifier equivalent to the Buyer's, the Seller's or the Facilitator's; must commit to the instrument it ran and its results | its Verdicts, superseded ones included | anyone holding the Delivery and the instrument |
| Facilitator | nothing | what it chose to sign, in the order it chose, on a clock that is its own | any holder of two of its Statuses, for equivocation; nobody, for omission or for time, without a witness outside this document |
The Facilitator row is the honest one. This protocol enforces nothing against a Facilitator; it makes some kinds of misbehaviour attributable and says plainly which ones it does not.¶
Equivocation, issuing two inconsistent histories for one contract, is attributable: every Status is signed, every Status's trace is a prefix of every later one, and two Statuses that break that rule are proof, checkable by anyone holding both, that the Facilitator signed contradictory records. A Facilitator that wants to make its records publicly append-only can register its Outcome Records with a SCITT transparency service [RFC9943] and hand the receipt [RFC9942] to the parties; this document does not require it and defines no log of its own.¶
Omission is not attributable. A Facilitator that declines to
record a Delivery, or records it late, produces no signed evidence of
having declined, and a Status it does not issue proves nothing. A
client SHOULD retain every Status it receives, and a party that
submitted a record and holds no Status for it has a claim it can
make only outside this protocol. Making omission attributable needs
a witness the Facilitator does not control, such as the client gossip that [RFC9162] Section 11.3 mentions and leaves undefined, and this document specifies none. A caller that relies on a performer's own word that a contract is complete, before the terminal entry is recorded, relies on something no record here carries, and the refusal of an early Outcome Record (Section 13) does nothing for it; what a party can ask for instead is the Facilitator's signed Status (Section 11).¶
Time is the Facilitator's. Every instant in a trace is read from its clock, and nothing in this document lets a party prove that a recorded instant is wrong. This document therefore states the assumption rather than hiding it: the Facilitator is a trusted timekeeper, and a deployment that cannot accept that should look to an external timestamping service, which this document does not specify and does not preclude.¶
Whatever a Facilitator does with anything of value under a terms profile is the profile's subject and is not addressed here.¶
A verification tier states how strongly work is checked. It does not state who checked it, and those fail separately. A re-execution transcript produced by the Seller and the same transcript produced by an independent Challenger are the same method and different evidence. Where a proof is generated and verified entirely inside one party, the tier is satisfied and the contract is unprotected. Section 9.1 requires that independence be derived by the evaluator from the party identifiers the contract names, and forbids satisfying it with a self-asserted field.¶
Absent an algorithm allowlist an attacker chooses the algorithm.
The two consequences are alg of none, which makes
every signature check vacuous, and presenting an ECDSA public key as
an HMAC secret, which lets anyone holding the public key forge.
Section 14.1 fixes the permitted set.¶
A kid carried as a sibling of the protected header rather
than inside it is outside the signed bytes and is rewritable in
transit. An attacker who can publish a key document can then
re-attribute a victim's genuine signature to an identifier it
controls, without breaking any cryptography.
Section 14.1 requires kid inside the protected
header.¶
Because every digest in this document covers a signature set, a
second valid encoding of one signature is a second digest for one
record. ECDSA has two valid s values per signature and JWS
does not choose between them; the low-S rule in
Section 14.1 does. The order of a signature set is a
second source of the same problem, and the sorting rule closes
it.¶
The -00 revision committed harness_uri as a string. The
bytes at that URI were covered by nothing. A Buyer could therefore
sign a contract, replace the acceptance instrument afterwards, run the
replacement, and submit its failure as a textbook-valid proof of nonconformance.
Cost of the attack: one file overwrite. The mirror attack works
against a Seller that hosts the input sample.
Section 5.1 requires a sibling hash over the dereferenced
bytes for every URI inside committed content, and
Section 7.2 requires a Verdict to commit to the
instrument it actually ran, which closes the same attack from the
verification side.¶
A work_uri, results_uri or any other URI in a
record is supplied by a counterparty and points wherever that
counterparty chose. An implementation that fetches it MUST fetch over
HTTPS only, MUST NOT follow a redirect to a scheme other than HTTPS,
MUST refuse to connect to a private, loopback or link-local address
(the ranges of [RFC1918], [RFC4193] and
their loopback and link-local counterparts), and MUST verify the
sibling hash over the full received bytes before any byte is used
for anything. A fetcher that acts on partial or unverified content
has handed its counterparty a way to make it execute, store or judge
something that was never committed to.¶
Naming a parent contract cost one signature in the -00 revision and was checked against nothing. Section 10.2 requires the child's Buyer to be the parent's Seller, checked by the Facilitator that holds the parent against the parent's own bytes.¶
The converse gap is stated rather than closed: a parent's Seller that never registers a failing child keeps it out of the parent's record, since this document compels no registration. A profile that wants children visible must make registration worth the Seller's while, or a Buyer that wants them visible must ask for the child's Status directly, which this document does not require the child's Facilitator to give it.¶
Wherever what a discoverer gains by reporting is less than what a performer loses by being reported, there is a private payment that leaves both better off than reporting, and silence dominates whatever reward a profile designed. This document cannot close that, because every figure involved is the profile's. What it does is record every Challenge, in order, whoever signed it, so that a profile can act on each independently, and it forbids a Facilitator from refusing a Challenge because the Buyer signed it (Section 7.3), so that the party with the most to recover is always admissible.¶
Under the -00 revision a contract in which nothing was ever delivered had no path to an end: the deadline carried no stated consequence and no window opened because there was nothing to challenge. Section 4.2 makes the deadline an event and ABANDONED a terminal state that every contract can reach. What reaching it costs anyone is the profile's, and a profile that makes delivering nothing cheaper than delivering something wrong has recreated the -00 incentive.¶
A VTC that does not name its Facilitator, network and asset is a
signed instrument replayable against any of them; Section 5
requires all three inside the signed content. A digest computed over
a contract excluding its signatures proves what was written and not
who agreed to it, so entries can be appended or stripped without
invalidating the commitment; Section 2 defines
every digest over the signature set. A Delivery, Verdict or Challenge
replayed against a different contract fails because each carries
vtc_id and a hash that binds it to one contract and one
Delivery, and the typ rule of Section 14.1
stops a signature over one object type standing for another.¶
A re-execution profile that does not state what determinism it assumes cuts both ways. An honest Seller doing model-assisted work is found wrong by a re-execution that differs for ordinary reasons. A cheating Seller escapes any proof of nonconformance by asserting nondeterminism, unfalsifiably. A verification profile MUST state whether it is deterministic and what tolerance applies, and a contract naming one that does not cannot be judged safely by anyone.¶
The argument for reputation derived from Outcome Records is that faking a history requires running real contracts. That argument fails if records do not name the parties or carry no Facilitator signature, since two cooperating identities can then manufacture history at the cost of two signatures. Section 12 requires both. It fails in the other direction if a negative outcome requires the signature of the party it records against; reputation that is structurally incapable of recording a loss is not evidence of anything.¶
A GET on a contract's Status or Outcome Record MUST be refused (retrieval-restricted) unless the requester is a party named in the contract's
parties, the identifier in the contract's
parent.facilitator, or a party the Facilitator has chosen to
admit; a Facilitator MAY open retrieval more widely and SHOULD say so in its capability document (retrieval, Section 3.9). How a requester proves which identifier
it is, on a GET with no body to sign, is an HTTP-layer matter this
document leaves to the deployment. The -01 revision left retrieval
unauthenticated by default, which published every contract graph a
Facilitator held to anyone who could guess an identifier.¶
A signature here is a long-lived commitment, and a compromised key
signs contracts the party never agreed to. Rotation and revocation
belong to the identity method behind the kid
(Section 14.1.1), and this document does not restate them.
Two things it does require: a Facilitator MUST retain, for as long as it retains a record it accepted, the key material or its digest as resolved at the time of acceptance, and SHOULD make it available to a party retrieving the record, so that a later rotation does not make an earlier signature unverifiable; and a Facilitator MUST NOT accept a record
whose kid resolves to a key the identity method marks as
revoked at the time of acceptance.¶
Every accepted Challenge costs an independent evaluation. Without a
cost to the Challenger, a party can exhaust a Verifier's or a
Facilitator's capacity by challenging every Delivery. A deposit required by a terms profile, advertised as challenge_deposit (Section 7.3), is one defence, and this document requires none, since a deposit also deters the honest Challenger an open model relies on. A Facilitator whose profiles require no deposit SHOULD rate-limit Challenges per
Challenger and per contract, and SHOULD publish that it does so.¶
PACT moves contracts and evidence about work, and both leak.¶
Publishing a representative input sample so that a counterparty can
price the work discloses production data to parties with whom no
contract exists and who may be in unknown jurisdictions. Samples
SHOULD be synthetic or de-identified. Where a real sample is
necessary, it SHOULD be disclosed only after a confidentiality
undertaking. Retention and deletion terms can be written into
constraints, where spec_hash commits to them, but
nothing in this document evaluates them and a PASS says nothing
about them (Section 3.3); a verification profile
that reads them says so (Section 9). This document
cannot enforce any of that and does not pretend to.¶
A Facilitator that publishes its Outcome Records makes the contract graph public. From it a reader can reconstruct an organisation's suppliers, spend and cadence, which is commercially sensitive even when no individual is identifiable. Transparency and counterparty privacy are in genuine tension here, and this document resolves it in favour of neither: retrieval is restricted by default (Section 17.12), a Facilitator MAY publish aggregates, and SHOULD NOT publish per-contract records identifying both parties without their agreement. Outcome Records leak the same graph by construction, since each names both parties and the counterparty retains a signed copy indefinitely. Selective disclosure over Outcome Records, so that a holder can prove a completed contract without revealing the counterparty, is possible with mechanisms specified elsewhere and is not specified here.¶
An open challenge model requires that some party outside the contract can obtain the deliverable and the input in order to build a proof of nonconformance. That is in direct conflict with confidentiality of both. The conflict is real and this document does not dissolve it. What it does is make the choice visible: a contract whose content cannot be disclosed to a Challenger will receive no Challenge from outside its parties, and a terms profile that counts on one has counted on nothing.¶
Retention periods stated for dispute purposes can conflict with erasure requests from a data subject. A retention period can be
stated in constraints, with the limits
Section 18.1 describes, and implementers should be aware that a hash
commitment survives deletion of the content it commits to, which is
usually the property they want and occasionally the one they must
explain.¶
This document asks IANA to register seven media types in the vendor tree and one well-known URI. It creates no registry. It defines problem types but does not ask for a registry of them (Section 19.3). The -01 revision asked for two registries, one of verification profiles and one of settlement bindings, and listed under the second an identifier in another project's namespace that nobody had defined; both requests are withdrawn. A profile of either kind is identified by a URI under its definer's control and needs no registration.¶
IANA is requested to register the following in the "Media Types" registry, per [RFC6838], in the vendor tree. The template below is given once in full; the seven registrations differ only in the subtype name and the object they carry.¶
application/json¶
| Subtype name | Object | Defined in |
|---|---|---|
| vnd.pact.contract+json | Verifiable Task Contract | Section 5 |
| vnd.pact.delivery+json | Delivery | Section 6 |
| vnd.pact.verdict+json | Verdict | Section 7.2 |
| vnd.pact.challenge+json | Challenge | Section 7.3 |
| vnd.pact.status+json | Contract Status | Section 11 |
| vnd.pact.outcome+json | Outcome Record | Section 12 |
| vnd.pact.facilitator+json | Capability document | Section 8 |
The -01 revision asked for these in the standards tree under the
names pact-contract+json and so on. Registration in that
tree from outside the IETF stream needs approval this document does
not have ([RFC6838], Section 3.1), and [RFC6838] Section 3.2 opens the vendor tree to anyone who interchanges files associated with a publicly available product.¶
IANA is requested to register the following in the "Well-Known URIs" registry, per [RFC8615].¶
This document creates no registry for its problem types.
[RFC9457] Section 4.2 establishes the "HTTP Problem
Types" registry for types intended for reuse across applications;
the types below are specific to this protocol and are identified by
URIs in a namespace this document defines, which that specification
permits without registration. Each is the identifier in the table
appended to the prefix
tag:laxsharma79@gmail.com,2026:pact:problem:, a tag URI
[RFC4151] under the author's control. A tag URI is an identifier and does not dereference. [RFC9457] Section 4 says a type URI SHOULD resolve to documentation; this document departs from that on purpose, so that an identifier does not change when hosting does, which the -01 revision's prefix on a code-hosting site could not promise, and the sections named for each type are its documentation. Each row
of the table carries the identifier, the HTTP status it accompanies,
and every section a problem of that type names in its
section member (Section 13.3); for
internal-error, which reports no rule, that is the section
defining it. A rule that another section restates, as
Section 10.2 restates a rule of
Section 10.3 and Section 14.2 the
rules of Section 5.3, is listed under the section a
refusal names, which is the one stating it. Where a row lists more
than one section, the section member of a problem names the
one stating the rule the request broke. A terms profile that refuses a request defines its own
types under its own prefix and reports them as
Section 13.3 says.¶
| Identifier | Status | Rules in |
|---|---|---|
| algorithm-not-permitted | 400 | Section 14.1 |
| amount-invalid | 422 | Section 14.2 |
| challenge-window-closed | 409 | Section 7.3 |
| child-outcome-invalid | 422 | Section 10.2 |
| deadline-invalid | 422 | Section 2, Section 14.2 |
| evidence-nonconformant | 422 | Section 6 |
| facilitator-mismatch | 422 | Section 13.1 |
| finality-ordering-violation | 422 | Section 10.3 |
| flow-unsupported | 422 | Section 7.1 |
| identifier-not-evaluable | 422 | Section 9.1 |
| internal-error | 500 | Section 13 |
| no-recorded-delivery | 409 | Section 7.2 |
| object-conflict | 409 | Section 6, Section 7.3, Section 10.2, Section 13.2 |
| parent-unresolvable | 422 | Section 10.2 |
| parties-not-distinct | 422 | Section 14.2 |
| payload-too-large | 413 | Section 13 |
| proof-nonconformant | 422 | Section 7.3 |
| retrieval-restricted | 403 | Section 17.12 |
| schema-invalid | 422 | Section 14.2 |
| settlement-unsupported | 422 | Section 13.1 |
| signature-invalid | 400 | Section 14.1, Section 14.1.1 |
| signature-missing | 400 | Section 14.2 |
| signatures-unordered | 422 | Section 14.1 |
| terms-parameters-invalid | 422 | Section 5.3 |
| terms-unsupported | 422 | Section 5.3 |
| unexpected-signer | 422 | Section 6, Section 7.3, Section 14.2 |
| unknown-contract | 404 | Section 13 |
| verdict-nonconformant | 422 | Section 7.2 |
| verifier-not-independent | 422 | Section 7.2, Section 7.3, Section 9.1 |
| wrong-state | 409 | Section 6, Section 7.2, Section 10.2, Section 13 |
The table is generated from the list the reference implementation in the repository named in Section 16 prints, so that every type an implementation of this document emits has a line here. The -01 revision listed eight of the twenty-nine its implementation used.¶
This appendix is not normative. It carries one terms profile,
under an example identifier and unregistered, so that the experiment
in Section 1.4 has something to run against and the
vectors in the reference repository have something to reproduce. It
is the -01 revision's settlement content written as a schedule over
the events of Section 4.2, with the choices the -01 revision left open now made and two of its own choices changed where the arithmetic or its text required (Appendix A.5), and it is offered as an example of the
form a profile takes, not as a recommendation of these terms. What the
figures below mean between the parties to a contract that names this
profile is a question this document does not answer and its author is
not qualified to answer; a profile meant for use needs an owner who is. Until such a profile exists, this one is also the only profile a Facilitator can list, since terms_profiles must have an entry; that is a fact about the present and not a rule of this document.¶
Identifier:
tag:laxsharma79@gmail.com,2026:pact:bonded-restitution.
The bundle in the reference repository, under
profiles/bonded-restitution/, contains
README.md (the prose of this appendix, in Markdown), parameters.schema.json and
vectors.json; profile_hash is the manifest digest
over those three files and Section 15 prints it.
Problem types this profile reports are under the prefix
tag:laxsharma79@gmail.com,2026:pact:bonded-restitution:problem:.¶
seller_bond:verification_fund:cap:restitution_basis:released or price.¶
remainder_to:buyer or sink; sink when absent.¶
verifier_fee:0.00 when absent.¶
principal_on:verdict (a PASS
Verdict), delivered, or window-closed.¶
assurance:mode
(certain, committed-sample or open),
q_min (a number greater than zero and at most one) and,
under committed-sample, sample_rate (a number
greater than zero and at most one: the declared fraction of
deliveries verified; the draw MUST derive from a seed the Buyer committed before the Delivery was submitted, combined with the Delivery's digest; how the seed is committed is outside the
profile).¶
This profile defines no Challenge deposit; a Facilitator that
advertises challenge_deposit does not do so under this
profile.¶
The -01 revision's four release modes map onto flow and
principal_on: on-verification is
verdict-first with principal_on verdict;
on-window is delivery-first with
window-closed; optimistic is
delivery-first with delivered; unsecured
is no-window with delivered.¶
Three internal accounts, opened empty: escrow,
bond, fund. External accounts, unbounded as sources
and sinks: buyer, seller, verifier,
challenger:<kid> for each Challenger, and
sink. Closure requires the three internal accounts to hold
zero after the last entry.¶
At accepted the profile evaluates, exactly and in the
contract's currency, with P the price, B seller_bond, q
assurance.q_min, and E equal to P when principal_on
is delivered and zero otherwise:¶
B >= P * (1 - q) / q + E
¶
and reports assurance-constraint-unsatisfied when it does
not hold, or when assurance.mode is open alone. The
inequality is the classical deterrence bound
([POLINSKY99]; [BELENKIY08] Theorem 1
for outsourced computation), with E the one term the -01 revision added: principal that moves before any Verdict is outside what the Verifier's check can withhold, so it raises what the Seller must post one for one. The bound deters nonconformance against that check and says nothing about what a later Challenge recovers; after a PASS is overturned the restitution of the schedule is bounded by the bond and the cap, whatever principal_on was. A
contract whose seller_bond or verification_fund
exceeds cap is reported as
parameters-inconsistent.¶
The rule is falsified, and this profile with it, if the constraint proves unworkable at the prices and verification costs real deployments exhibit. That was the -01 revision's own failure condition, restated here where the rule now lives.¶
For each event the schedule emits the entries below, in the order
listed, omitting any entry whose amount is zero. Every event of
Table 2 not named here emits nothing. Amounts
are computed from the contract and the trace prefix; "released" is
the sum of principal entries emitted so far. Every amount
is computed exactly and emitted exactly: the schedule never rounds.
Each is a sum, a difference or the lesser of amounts in the
contract, the trace or the entries before it, so it has no more
decimal places than they have, and it is written with the fewest
decimal places that keep its value, never fewer than two.
Section 14.2 refuses a price, or a Challenge's costs,
with more decimal places than the binding supports; this profile does
not hold its own amount parameters to that limit, so an entry has up
to as many decimal places as the amounts that produced it, which can
be more than the binding supports.¶
funded:lock;
seller to bond, B, bond; seller to fund,
verification_fund, fund.¶
delivered:principal_on is
delivered: escrow to seller, the escrow balance,
principal.¶
verdict:verifier_fee and the fund balance,
verification; then if the outcome is PASS, no Challenge is
answered, and principal_on is verdict: escrow to
seller, the escrow balance, principal.¶
window-closed:principal_on is
window-closed and the standing Verdict is not FAIL: escrow
to seller, the escrow balance, principal.¶
terminal, FINAL:principal; bond to seller, the bond balance,
return; fund to seller, the fund balance,
fund-return.¶
terminal, ABANDONED:reverse; bond to seller, the bond balance,
return; fund to seller, the fund balance,
fund-return. The -01 revision said the bond was slashed "to the extent of" the basis here, and its Section 5.3 defined the basis as an amount, the value already released or the full price, without relating either to a loss; with the price reversed the Buyer's loss is zero under either basis, so this profile slashes nothing here, which under basis price is a departure.¶
terminal, SETTLED:reverse. (2) if challenge_upheld: fund to the
Challenger whose Challenge the standing Verdict answers, the lesser
of that Challenge's costs when stated in the contract's currency (otherwise nothing) and the fund balance,
costs. (3) bond to buyer, the lesser of the bond balance, the cap room and the Buyer's loss, restitution; the loss
is "released" under basis released and P minus the rank-1
entry under basis price, which coincide under this schedule, since every principal entry moves the whole escrow balance; the parameter is kept for a profile that adds partial release. (4) if challenge_upheld: bond to that Challenger, the lesser of the bond balance and the cap room, bounty. (5) bond to buyer or sink per remainder_to, the lesser of the bond balance and the cap room, remainder. Then bond to seller, the bond balance, return, which is what the cap kept; then fund to seller, the fund balance, fund-return. The cap room at each rank is cap less what the entries so far have moved out of the bond.¶
Ranks 2 and 4 pay one Challenger, the one whose Challenge the standing Verdict answers. The -01 revision required the reward to be non-exclusive, paying every independent discoverer in full; one bond cannot fund that for two discoverers, so this profile pays one and records the departure here. A Challenge that was not answered by the standing Verdict, whether lapsed, rejected or superseded, receives nothing. Rank 4 gives the whole remaining bond, because the -01 revision forbade capping it at a fraction chosen for tidiness and fixed no figure; a profile owner who wants a different rule changes this line and the vectors with it.¶
With P 180.00, B 18.00, fund 0.50, cap 180.00, basis
released, remainder to sink, no verifier fee,
principal_on verdict, assurance certain
with q 1.0, under the verdict-first flow, and a Challenge
claiming costs of 1.20. Amounts are in USDC. Trace indexes count
from zero. The lists below are what vectors.json carries
for the two paths in the figures of this document. The repository's
file also carries the SETTLED-by-Verifier, ABANDONED, verdict-lapsed
and delivery-first paths, the price basis, two FINAL paths
with a verifier fee of 0.105 against funds of 0.51 and 0.50, and
three admission vectors, one refused and one admitted at the
boundary of the constraint. The third is refused: at P 0.01 and q
0.3333333333333333 its bond of 0.020000000000000003 is about
3 x 10^-34 below the bound of Appendix A.4.¶
trace 0 accepted 1 funded 2 delivered 3 verdict PASS
4 window-opened 5 window-closed 6 children-final
7 terminal FINAL
event from to amount code
1 buyer escrow 180.00 lock
1 seller bond 18.00 bond
1 seller fund 0.50 fund
3 escrow seller 180.00 principal
7 bond seller 18.00 return
7 fund seller 0.50 fund-return
trace 0 accepted 1 funded 2 delivered 3 verdict PASS
4 window-opened 5 challenge 6 verdict FAIL (answers 5,
supersedes 3) 7 children-final 8 terminal SETTLED,
challenge_upheld true
event from to amount code
1 buyer escrow 180.00 lock
1 seller bond 18.00 bond
1 seller fund 0.50 fund
3 escrow seller 180.00 principal
8 fund challenger:<kid> 0.50 costs
8 bond buyer 18.00 restitution
In the second vector rank 1 emits nothing because the escrow is empty, rank 2 pays the lesser of 1.20 and the fund's 0.50, rank 3 pays the whole bond because the Buyer's loss (180.00 released) exceeds it, and ranks 4 and 5 and the fund return emit nothing because nothing remains. Both lists satisfy closure: after the last entry the three internal accounts hold zero.¶
The -02 revision [I-D.laxsharma-pact-02] lists what changed from -01.¶
https form and in the encoded did:web
form.¶
https
path of exactly "/" is removed also when a query follows it (V-43).
The -02 text kept such a path. The -02 reference implementation also
compared such a "%" as written, although the -02 text already made an
identifier that does not parse not evaluable.¶
work_hash.¶
constraints and that a PASS speaks to
acceptance only. Section 18.1 no longer
sends retention and deletion terms there as though something read
them, and Section 9 asks of a verification profile a
sixth statement: whether it evaluates any of constraints,
and how a Verdict reports the result, and says that a result on
constraints never changes outcome.¶
task.spec_hash,
verification and flow from the contract, and
outcome.verdict names the standing Verdict or says
verdict-lapsed or none, so a verified PASS, a
lapsed Verdict and a flow without Verdicts no longer end in the same
record. Section 12 requires work_hash to be
the recorded Delivery's. V-45 to V-47 and V-62 test
outcome.verdict, and V-44 tests work_hash.¶
pact version is 0.3. The Outcome Record gained
required members (Section 3.8), and
Section 2 makes extension a new version rather
than added members.¶
terminal entry
is outside what the records reach.¶
price.amount and
costs.amount, with V-49 for the second.¶
schema_uri, sample_uri and
rubric_uri, beside V-11 for harness_uri. V-54
and V-55 test a child's Outcome Record posted to the
parent's Facilitator (Section 10.2): accepted
when the Facilitator the registration recorded signed it, refused
when another did.¶
terminal entry, one for
a child with no record held. Both cite that section.¶
outcome.verdict. V-61 is a record re-signed by
the child's own
Facilitator with verification altered.¶
{id} is
percent-encoded as one path segment, and the contract id
entry of Section 3.2 excludes "." and "..", which a
client resolving {contract}/{id} would remove as dot
segments ([RFC3986] Section 5.2.4). V-56 is a
contract with such an id.¶
profile_hash changed, because the profile bundle gained
three vectors and a paragraph on exact amounts
(Appendix A.5). The digests of the contract,
the Delivery, both Verdicts, the Challenge, the Status, the Outcome
Record and the capability document changed with it, and with the
pact version. The Outcome Record example also gained the
members above. spec_hash and criteria_hash are
unchanged.¶
Twelve defects in the -02 text and its reference implementation, found after -02 was posted, are corrected as well.¶
did:WEB:, and
compared it as written. Steps 5 and 6 of Section 9.1 now say
what parses. An identifier in neither form of Section 14.1.1
is not evaluable, and the refusal has its own problem type,
identifier-not-evaluable, which V-35 and V-36
expect.¶
https identifier carrying a
character no URI contains, such as "\", which some URL parsers read
as "/". A Seller could name its own identifier, spelled that way, as
Verifier and sign the Verdict with its own key. Step 6 now requires
an https identifier to be a URI; V-37 is that
contract.¶
kid.¶
price.amount and of no other amount, so a Challenge whose
costs had three decimal places was accepted. It now checks
costs.amount as well (V-49).¶
internal-error.
Section 14.2 now refuses at acceptance a contract
whose latest finality instant falls after the year 9999, and V-50
is one.¶
internal-error. Appendix A.5 now says
the schedule never rounds, and two bundle vectors exercise it.¶
event and
at and nothing else, so a Status or an Outcome Record
whose window-opened entry had no closes_at, or
whose accepted entry carried one, validated, and the
reference implementation recorded such a child Outcome Record as
child-final. The schema now requires the members
Table 2 requires for each event and admits no
other (V-51, V-52). Section 3 pointed to
Section 14.2 for the rule on omitted and undefined
members, but that section requires schema conformance and does
not state the rule, and Section 2 stated only
its second half. Section 2 now states both
halves, and the pointer names it.¶
child-unresolved entries of a
parent's trace name the child records its
children_merkle_root does not cover. A child's record that
arrives after that entry and before the parent's terminal
entry is still recorded as child-final, and the root covers
it, as the -02 reference implementation already did for a posted
record. Section 12.2 now says so, and the reference
implementation also picks up such a record from its own
venue.¶
On the IETF dispatch list in September 2026, Rich Salz read the -01 revision as a legal framework with a protocol attached and said so, and John C Klensin wrote that the framing of its terms was tied closely enough to legal terminology that the IETF was the wrong place to evaluate it. Both were right; the -02 revision's split between records and terms is the consequence, and the author is grateful for the reading. The UTF-16 key-ordering vector that exposed a latent canonicalization defect in the reference validator, and the formulation of verifier independence as a relation the evaluator derives rather than a field the record declares, came from Tersign (wowlegend) on x402-foundation/x402 issue 3065. The observation that verification tiers say how work is checked and never who checks it came from msaleme on the same thread. Rich Smith's A2A Settlement Extension was the clearest instance of the pattern the -01 revision corrected, and he engaged with the critique on a2aproject/A2A discussion 1576.¶
On pact-spec/spec issue 12 in September 2026, msaleme read the
whole of the -02 revision [I-D.laxsharma-pact-02]
against the -01 and found that nothing in
this document evaluates constraints, though a PASS could be
read as covering them; that an Outcome Record did not say what was
verified, ended a verified PASS, a lapsed Verdict and a contract with
no window in the same FINAL, and set no rule for its own
work_hash; that Section 6 never required a Delivery's
evidence to name the instrument the contract committed to; and that
the -02 identifier rules could not both fold equivalent identifiers
together and forbid decoding. A first finding, on how a Verdict
reaches the delivered bytes, was withdrawn in full by msaleme in a
second public correction, and still led to the sentence in
Section 7.2 that says how. In the exchange that followed, msaleme pointed out
that refusing an escaped unreserved character is stricter than
[RFC3986] and should say so, and that the -02 promise
to fold toward the same party claimed more than a comparison of
strings can deliver. The cases behind V-27 to V-30, V-32 and V-33
are the ones msaleme named. msaleme also read this revision against
the -02 before it was posted; V-62 and the sentence in
Section 9 on constraint results answer that
reading.¶