response_contract check. A mismatch is a proven FAIL, and a FAIL blocks the release.
The format
A contract is a JSON object with three optional fields and nothing else:contract.json
integer
default:"200"
The expected HTTP status of the paid response, from 100 to 599.
string
The expected media type, written as
type/subtype, at most 200 characters. It is compared case-insensitively with the media type of the response’s Content-Type, ignoring parameters, so application/json; charset=utf-8 matches application/json. Omit it to skip the check.object
A JSON Schema the parsed response body must satisfy, at most 16 KiB once serialized. Omit it to skip body validation.
{} checks only that the status is 200.
The file has the shape of the expected field of POST /v1/release-test. selo test --contract contract.json sends it as expected, and a raw API client sends the same object. See Pay with any x402 client.
How a contract is compiled
The schema is compiled with Ajv 8 in strict mode, using Ajv’s default JSON Schema dialect, draft-07. A contract is compiled twice before anyone pays:- by the CLI, which exits
3before sending any request; - by Selo’s API, which answers
400 VALIDATION_FAILEDwith the messageexpected: <issue>before asking for payment.
the contract file <path> is not usable:, and the API after expected:. Schema errors start with jsonSchema failed to compile:, followed by Ajv’s own message.
A top-level key other than
status, contentType and jsonSchema is rejected before compiling. The CLI prints the contract file contract.json must be { status?, contentType?, jsonSchema? }: Unrecognized key: "expected", and the API answers 400 VALIDATION_FAILED.
These compile without trouble: title, description and examples; const and enum; pattern on a string; nested objects and arrays; $ref into definitions or $defs; a nullable type written as ["string", "null"]; and "$schema": "http://json-schema.org/draft-07/schema#".
How a paid response is evaluated
Selo reads the paid response body, up to 256 KiB, then checks in this order and reports the first mismatch:1
Status
The response status must equal
status.2
Content type
If
contentType is set, the response’s media type must match it.3
Body
If
jsonSchema is set, the body must be non-empty, valid JSON, and satisfy the schema. Only the first schema error is reported.
A
SCHEMA_MISMATCH summary is the error’s location and message, cut at 200 characters. response_contract is a blocking check, so a FAIL with evidence makes the verdict FAIL. See The five release checks and Verdicts and the gate.
Worked example: the demo merchant
The demo merchant sells one order and returns a receipt. Its contract is in the Selo repository atexamples/merchant/contract.json:
contract.json
200 with application/json after the payment settles. The difference is one field name:
response_contract results:
A health check, a status check and a content-type check all pass on
broken-v1. Only the schema catches the renamed field. The extra fields sku, quantity and release do not matter, because the schema does not close the object.
Which contract did a verdict use?
Every signed attestation recordsexpectedContractSha256: the SHA-256 of the contract as canonical JSON, with keys sorted at every level and no whitespace. You can recompute it from your file:
Terminal
Output
expectedContractSha256 in both of the demo merchant’s Mainnet attestations. See Verify a signed verdict.
Tips for a good contract
Require what your clients read
Require what your clients read
List in
required every field a buyer’s integration depends on, and nothing it does not. A field you plan to remove should not be required.Leave the object open unless the shape is closed
Leave the object open unless the shape is closed
additionalProperties: false turns every new field into a FAIL. Use it only if adding a field really breaks your clients.Use pattern and const instead of format
Use pattern and const instead of format
No
format is available, so date-time, email or uuid fail to compile. Use pattern on a string for id and date shapes, and const for values that must never change, such as "fulfilled": { "const": true }.Declare the type next to every type-specific keyword
Declare the type next to every type-specific keyword
Strict mode wants
"type": "object" next to required and properties, "type": "string" next to pattern, and a numeric type next to minimum or maximum.Set the status and content type explicitly
Set the status and content type explicitly
status defaults to 200, and the content type is not checked unless you set it. A route that starts answering 201 or text/plain is caught only if you say what you expect.Keep it in the repository, next to the code
Keep it in the repository, next to the code
Commit the contract, for example as
selo/contract.json, so it changes in the same pull request as the response. The demo merchant has a test that keeps contract.json equal to the receipt schema it advertises in its Bazaar listing.Fix one error at a time
Fix one error at a time
Only the first schema error is reported. After you fix it, another mismatch may surface on the next run.
Next
Run a release test
Pass the contract with
--contract and gate on the verdict.Try it with the demo merchant
Reproduce the FAIL and the PASS yourself.