SchCP v0.4 design notes: arranging time, never money

Decisions from a discussion with the author on 2026-10-15. v0.3 is the version the business reviews and the whitepaper up to v0.9 describe. v0.4 changes what SchCP is for. §8 records the decisions made in live testing with AI assistants on 2026-10-15 and 16.

1. The decision

SchCP never moves money. It exists to arrange meetings: a person and a business, or two people. It is a first step toward agents coordinating people's time on their behalf, with the human's signed consent at each commitment.

Two observations led here.

2. What v0.4 removes

3. What stays, re-read

4. What v0.4 adds

4.1 Hosts and guests

"Merchant" became host and "customer" became guest, everywhere: data model, wire, errors and reasons. A host has an optional kind, either business or person.

4.2 Open-ended bookings

duration: "open" means an agreed start and no agreed end. The host sets time aside so nothing is booked on top of it: set_aside as a duration, or by default until_close, the rest of the working day, up to the first close among the resources it takes. Slots, holds, offers and bookings carry open_end: true and no end. A mandate's window bounds only the start. The host ends the booking when it is over. Ending frees the rest of the time set aside, completes the booking, and sends booking.ended with a receipt that carries the real end. An open booking cannot be extended, and it cannot be ended in advance.

Examples: a car dropped off for diagnosis, a playdate, a consultation that runs as long as it runs.

4.3 Places

place replaces service_location. A service happens at the host's (the default), at the guest's address (guest: postal code and country, inside any service area), or elsewhere: a place the guest names, such as a park or a café. The place is part of the signed offer and the calendar event's location.

4.4 People as hosts, and contact lists

A person's own agent keeps their calendar as a host, possibly on a personal-calendar service acting as operator (Daybook in the sample world). policies.access: "contacts" means only the host's owner and the principals on its contact list may see availability, hold, join the waitlist or confirm. Anyone else is refused with NOT_PERMITTED and reason not_a_contact. The list is never published. Availability then needs a mandate, to show who is asking. A contact removed between hold and confirm cannot confirm.

4.5 Meetings of several people

The user's direction (2026-10-15): "contract is 3 members, hashing matches across all 3". A meeting is one contract every member accepts by the same hash. Two people are just the smallest meeting: arrangeMeeting uses the same flow for two or for ten.

  1. The organizer's agent issues one mandate naming every member's calendar (host), reads them all, keeps the starts every calendar offers, and holds that time on each.

  2. It writes one meeting contract (MeetingClaims, a JWS with typ schcp-meeting+jwt):

    • the title, start, end or open_end, and the place;
    • each member's principal, name and calendar;
    • the SHA-256 of that calendar's own signed offer for the time.

    A person's calendar names its owner in its offers (host.owner), which is where the member principals come from. Offers only reach contacts, who know each other's principals anyway.

  3. The organizer's human approves the text once. The key app's signature over the contract is that approval; the statement inside is generated from the claims.

  4. Each calendar gets the contract with its confirm (ConfirmRequest.meeting) and checks its own entry:

    • the contract is signed by the guest;
    • the member named for this calendar is its owner;
    • the offer hash is this hold's offer;
    • the time and place are this offer's.

    Every calendar makes the same check against its own offer, so the contract all of them accept says what every offer says. No calendar needs another's keys.

  5. Each member's calendar then waits (pending_host) for that member's acceptance: a JWS with typ schcp-acceptance+jwt naming the contract's hash. It comes from their own key, or from their agent under a rule they signed (§4.6). Accepted, the booking is tentative. The organizer's own calendar is tentative from the start; its owner signed the contract.

  6. Commit. When every member has accepted, the organizer's agent brings all the acceptances to every calendar (POST /bookings/{id}/commit). Each one verifies them against the same hash and confirms. Nothing is confirmed anywhere until everyone has said yes. Every receipt names the one contract hash.

Anyone declining, or the contract's exp passing, ends it: finishMeeting cancels the rest, and the sweeper lapses tentative meeting bookings with reason meeting_incomplete. If a commit fails partway (one calendar lapsed), the agent cancels the others, which costs nothing (§5). Coordination is the organizer's agent's job; hosts never talk to each other.

Children and anyone else who answers nothing are named attendees; required participants with their own principal are refused in a meeting, because the members are the people who answer.

4.6 Who says yes: a person, or a rule they signed

The user's direction: "a host's agent can approve by rule (but again, prefer strongly human confirmation)". So:

Earlier notes put rules "in the host's agent, not on the wire". Meetings changed that. Another member's calendar has to be able to check a yes it did not see given, so a rule acceptance carries the rule. The check: the same person signed it, it names the key that signed the acceptance, it was unexpired when used, and it covers this service, day and organizer.

Businesses keep confirmation: "instant" for ordinary bookings. Their published terms are the standing yes, and the guest's person always approves. A meeting member's calendar always needs an acceptance, whatever its confirmation says.

4.7 Counters: the other side names a different place

The user: "guest names it is ok but the other should be able to counter". A host whose booking is waiting for its answer can answer with other terms instead (hostCounter): a different place, or a different start.

A confirmed meeting is not revised. To change it, arrange it again. That is the smallest rule that keeps "everyone agreed to the same hash" true; post-commit revision is an open question. (Superseded on 2026-10-17, §8.14: a confirmed meeting keeps its time until every member agrees to the change.)

5. Cancellations and no-shows are the host's business

The user's direction (2026-10-15): "it makes no sense for us to have no shows and cancellations recorded, let's leave that business logic up to the hosts agent/system (since we are not dealing with money any longer)".

Why. Outcome records were the last piece of the money machinery. Once nothing was charged, a record with an amount was a debt the protocol described but never collected. Keeping it meant fee tiers in every offer, a quote route, an undo window to make early cancellations free, a closed-mandate mode keyed on no-show fees, a mandate ceiling for those fees, and a deadline rule for tentative bookings so they settled before a cancellation "had teeth". None of that arranges time. A host that cares about late cancellations already has its own system and its own relationship with the guest.

What was removed.

Cancelling always succeeds, costs nothing and records nothing: cancelled, sequence + 1, a fresh receipt. Change offers carry no records, and a missed reconfirmation releases the booking with nothing recorded. The conformance checks cancel.quote and cancel.undo became one, cancel.free, and the runner's --late-window option went with the late window.

What was kept, and why. A host may still mark a past booking completed or no_show, as a status only: no record and no amount. The booking.no_show webhook (with sequence + 1 and a new receipt) stays, and attendance is still declared by the host, never inferred. The reason is the sybil bound. max_active_unproven (spec §7.2) limits how many holds principals without a kept appointment may hold at once, and eligibility: {guest: "returning"} admits only principals with one. A kept appointment is a booking whose time passed without being cancelled or marked a no-show. Making or cancelling a booking is free, so neither can earn standing; if the host could not mark a no-show, a script could book, not turn up, and graduate each throwaway identity into a returning guest. The status is the minimum that keeps the bound honest. Prices as disclosed terms, max_price, and the refusal of payment data are unchanged.

Consequence worth watching. Without the undo, a cancellation within 60 seconds of confirming keeps the confirmed credit for hold reputation (spec §7.3), as a later cancellation always did; v0.3 counted it as a released hold. Little changes in practice, since an agent could already wait out the minute, but booking and cancelling remains a cheap way to dilute an expiry ratio. It never earns standing.

6. What changed on the wire since v0.3

Nothing has been released, so none of this needs a migration path; the list is for reviewers who know the v0.3 draft.

7. Open questions

Recurring meetings, revising a confirmed meeting, businesses as members, who the members are and finding a place were decided on 2026-10-17 (§8.14–§8.18), and consent fatigue too (§8.13), as was the question of converging with the business-booking efforts (§8.19). The questions still open (accessibility requests, attesters, tuning, who hosts people, whether businesses accept a protocol that never charges, platform risk, governance, an independent implementation and wider live testing) are listed in the whitepaper's §11.

8. Decisions from live testing

On 2026-10-15 and 16 the MCP tools were tested live, with AI assistants (Claude Haiku) on both sides of each booking, talking through a local gateway to the simulated hosts. Assistants played the guests (Noor, Sam, Kim, Alex) and the hosts: the front desk of Rosie's Nails, and the agents keeping Ellis's and Priya's calendars. Each round ran three scenarios. After each round the defects and the wording the assistants misread were fixed, until three rounds in a row were clean. That took 25 rounds; the clean ones were 17, 19, 21, 23, 24 and 25. Many fixes were wording in the MCP tools; the decisions below changed the protocol or the way the tools present it. Each commit names the round that found its fix.

8.1 A host_approval host approves a guest's move too

Round 15: Sam moved a confirmed pedicure at Rosie's Nails, which approves each booking, and the booking went straight to the new time without the salon's yes. A host that approves each booking has chosen to look at every time it gives away, and a move gives away a new time.

Accepting a guest's change offer at such a host now leaves the booking as it was (time, contract, sequence) and marks the accepted offer awaiting_host, with approved_by (now consent, §8.12) saying what the guest's key app signed. The new time stays held until the host's approval_timeout, and never past the booking's own start. The host's yes makes the move atomically; its no, or no answer, releases the new time. The booking keeps its time meanwhile so that a no or silence leaves the guest where they were, never without a booking. A rule the person signed may say yes, as for a new booking, and the owner's own moves need nobody's yes. A move the host never answered does not count against the guest's reputation: the guest did their part. Webhooks booking.change_approved and booking.change_declined (with a refusal) carry the answer. Spec §5.7 step 3, §5.8, §6.

8.2 A declined move is kept on the booking

Round 16: Sam's agent polls rather than taking webhooks, and could not learn why Rosie's said no to a move: the refusal travelled only in booking.change_declined. Webhooks are an optimization and agents must be able to poll (spec §5.8), so the booking itself now records declined_change (proposal_id, start, place, refusal) until another change is asked for. The MCP's get_booking turns it into move_result, quoting the host's message or saying the host did not answer in time. Round 18 added the case where the guest cancels while a move waits: move_result then says the move was dropped because the booking is cancelled, not that the booking kept its time.

8.3 Per-member answers in a meeting

Round 16: Noor was happy for Priya's weekend rule to answer, but wanted Ellis to answer in person. answers was one value for the whole contract, so she could only be stricter than she wanted for Priya or looser than she wanted for Ellis. A member entry may now carry its own answers, which overrides the contract's for that member, and the organizer's consent text names each group ("Priya Shah answers in person; Ellis Moreau may answer by a rule they signed"). The MCP's arrange_meeting takes in_person, a list of host ids. Spec §5.12, §7.9; schema.

8.4 Taking back a yes, and backing out

8.5 A confirmed meeting is changed only by calling it off

Round 14: asked to push a confirmed coffee 30 minutes, revise_meeting held the new time on every calendar, asked the organizer, signed revision 2, and was refused only then, leaving stray change offers on the members' confirmed bookings. The rule was already that a confirmed meeting is not revised (§4.7): changing it booking by booking would break "everyone agreed to the same hash". The refusal now comes first: the planner checks before holding anything or asking anyone, and the engine refuses a change offer on a confirmed meeting booking (meeting_invalid). The tool says to call it off with cancel_meeting and arrange it again. Round 9 had added cancel_meeting: an assistant was cancelling booking by booking, and another calendar briefly still showed the meeting. Round 23: an assistant was surprised that a confirmed meeting went off when a member backed out; meeting_status now says a confirmed meeting can still go off. Revising a confirmed meeting remains an open question (§7). (Superseded on 2026-10-17 by §8.14.)

8.6 Choosing a person is exposed in the MCP, with resource_name in offers

Round 20: Alex wanted a haircut with Marcus at Fades on Queen. The protocol already lets a guest choose a selectable resource, but the MCP tools had no way to. discover_host now lists choose_from per service, and find_slots and hold_slot take with (a name or an id). The offer named the chosen resource only by its id, so the consent text could not say who in the words people use. Offers now carry resource_name next to resource, inside the signed claims, so the name the user approves is the host's own: "Haircut with Marcus at Fades on Queen". Spec §5.2; schema.

8.7 user_signed in the MCP

Rounds 15 to 21: assistants kept reading approved_by: "agent" as "the user did not approve". On the wire the field is precise and stayed (since §8.12 it is consent.approved): it says whether the principal's own signature covered this exact offer, or the agent accepted it within an open mandate. The MCP result now states it from the user's side: user_signed is "this exact offer" or "only the limits: service, time window, price ceiling" (round 24 found "the limits" alone unclear), and the text beside it starts from the user's approval. The same reasoning earlier (round 5) renamed discover_host's human_signature to user_signs_each_offer, which assistants had read as "the host's yes is not signed".

8.8 Daily hours in find_slots

Round 22: one page of daytime slots could crowd out the evenings a user asked for. find_slots takes daily: { from, to }, the same hours of each day in the window, local time at the host, and asks for each day separately so earlier hours cannot fill the page. Round 23: to may be "24:00", because an assistant had to write 23:59 for "6 pm or later". This is MCP only: the wire's availability request takes one window, and the tool makes one request per day.

8.9 A business's own answer is by: "host"

Rounds 15–25: a business's front desk that approved or declined a booking was recorded as answer: { by: "person" }, the same as a person answering on their own calendar. Assistants read "person" as a private individual rather than the salon. Decided on 2026-10-16: a business's own answer is by: "host". "person" is kept for the person whose own calendar it is (host.kind: "person"), and "rule" for a rule that person signed. This covers approvals, declines, a counter the guest accepts, and a move the host approves. Acceptances (schcp-acceptance+jwt) keep by: "person" | "rule", because only a person signs one. Spec §7.9, §5.5, §5.7.

8.10 Cancelling gives a booking back to a shared limit; "wallet" and "budget" renamed

Decided on 2026-10-17. The person's app that holds their signing key was called a "wallet", and a mandate's limit counted across several hosts an "online budget". Both words sound like money, and SchCP never moves money, so they are now the key app and a shared booking limit: constraints.budget: "online" became constraints.shared_limit: true, and the refusal reason budget_exhausted became shared_limit_reached. Nothing has been released, so there is no alias.

A booking that will not happen now gives its unit back. Before, cancelling kept the claim, so "up to 3 haircuts at any of these 4 barbers" became 2 after one cancellation. Now a booking cancelled by either side, declined, or expired gives its claim back once (DELETE {status}/claims/{claim_id}); a guest's cancel does so before it answers, so the agent can book again at once. A key app that does not answer never stops a cancel: the host retries for a day, then leaves the claim held, so the person has one booking fewer left, never one more. A booking that went ahead keeps its unit. Hold limits and rate limits still stop an agent cancelling and rebooking in a loop. Spec §3.3.

Why: the user's reason: the words sound like money and SchCP never moves money; and people expect a cancelled booking not to use up their limit.

8.11 Each host states how long it keeps booking records

Decided on 2026-10-17. A booking at a clinic is itself health information, and a person-host keeps records about their friends, yet nothing said how long a host keeps them. Each host now states it: policies.retention in discovery (required), repeated as retention in every offer (required), and in every receipt. It is a duration in weeks or days counted from the end of the booking (or its cancellation); "P0D" means not past its end. SchCP sets no number, because laws and trades differ: the sample garage keeps a year, the clinic ten years, the parents a month. The consent text states it ("Maple Street Auto keeps my booking details for 1 year after the booking"), and like every other term it is the contract's: a change offer repeats the booking's own retention, not the host's current policy. SchCP does not check that a host keeps its word. Spec §2.1, §5.2.

Why: the user chose "host states it": the person sees it before saying yes, and SchCP stays out of setting a number that differs by place and trade.

8.12 Receipts carry how the person agreed, under which mandate, and the retention

Decided on 2026-10-17. To serve as evidence in a dispute over terms, a receipt now carries:

The agent SDK refuses a receipt whose retention differs from its contract's, and, for a booking it just made or changed, one that names another mandate. Two vectors in moves.json pin this. Spec §5.3.

Why: the user's decision: small additions that make a receipt usable as evidence without adding any money or policy.

8.13 What an assistant shows before the person says yes

Decided on 2026-10-17. With no price to anchor attention, a person may approve everything unread. Spec §5.3 now says an assistant SHOULD show the exact consent text, SHOULD point out what differs from what the person asked for (another time, place or person, a price they did not mention or above their limit) and how long the host keeps the records, and MUST NOT say yes for the person except under a rule or limits they signed. The MCP server says the same in its instructions, and hold_slot returns differs_from_request and offer.records_kept.

Why: the user chose short guidance over leaving it to each assistant.

8.14 A confirmed meeting keeps its time until everyone agrees to a change

Decided on 2026-10-17. Before, a confirmed meeting could only be called off and arranged again, so for a moment everyone lost it, and a member who never answered the new time left everyone with none. Now the organizer revises a confirmed meeting as it revises one still gathering its yeses (change offers, a higher revision, the human's approval of the new text, POST /bookings/{id}/meeting), but each calendar keeps the booking confirmed at its time and holds the revision beside it as meeting.pending_revision, with the change offer held until the revision's deadline. Every member answers the revision's hash on their own calendar (a rule, the organizer's own signature, or a business's offer answer at once). Once every member has, the organizer commits the revision everywhere, businesses first and its own calendar last, and each calendar moves atomically (sequence + 1, booking.change_approved). A no, a yes taken back, or the deadline passing drops the revision on that calendar (declined_change with the revision number and the refusal), and the organizer withdraws it from the others (POST /bookings/{id}/meeting/withdraw, reason organizer_withdrew). The revision's exp must be no later than the meeting's current start, so everyone is asked before it begins, and revision numbers keep rising past one that did not happen. If every member said yes but one calendar can no longer move, the calendars disagree, and the organizer's agent tells its person and revises again or calls it off; finishMeeting reports that as split. This mirrors a guest's move at a host that approves each booking (§8.1). Spec §5.12, §6.

Why: the user's answer: keep the old time until all agree; if anyone says no or does not answer in time, it stays as it was, so nobody loses the meeting.

8.15 A business can be a member of a meeting; its offer is its yes

Decided on 2026-10-17. Three families want the playdate at a café, with a table held for them. A business that takes part in meetings is now a member with business: true, named by its host principal (urn:schcp:host:<host id>). It has no person to accept: its signed offer for the slot is its yes, and a business that approves each booking says yes as the host (answer.by: "host", no acceptance). The meeting is final only when every person has accepted and the business has said yes; a business that declines or lapses ends it like a person's no. Calling the meeting off cancels the business's booking too. The organizer is always a person, and a meeting needs at least one other person. Where the business's service happens on its own premises, its offer names no place, and its calendar does not compare the contract's place. The simulated world gained Bellwoods Café. Spec §5.12.

Why: the user's answer: yes, the business's terms are its yes, which is how ordinary bookings already work.

8.16 Calendars say they take part in meetings

Decided on 2026-10-17. A calendar that takes part in meetings advertises the meetings capability in discovery: every person's calendar does, and a business opts in (HostConfig.meetings). An organizer's agent checks it before holding anything (arrangeMeeting says which calendar does not), the MCP's discover_host reports takes_meetings, and a calendar without it refuses a meeting contract (meeting_invalid). Spec §2.1, §5.12.

Why: the user's answer: an organizer's assistant should be able to tell beforehand, instead of finding out by trying.

8.17 Members are shown by your own contact names

Decided on 2026-10-17. The other members' names in a contract are the organizer's word. A person's key app now keeps their own contacts (principal → name), and every meeting text it signs or shows names other people from them; a member it has no contact for is "someone you don't know, named "Priya Shah"", never only the organizer's label. The MCP shows members the same way. A business is named as its own signed offer names it. Spec §5.12 (SHOULD).

Why: the user's answer: show members by the user's own contacts, or say it is someone they do not know.

8.18 Recurring meetings and finding a place

Decided on 2026-10-17: recurring meetings are not in v0.4 (each occurrence is its own meeting, and an assistant can arrange the next one), and finding a place both sides like is out of scope (the guest names it and the other side may counter; where to meet is the people's own business, like getting there). Spec §11.

Why: the user's answers: not in v0.4; out of scope.

8.19 SchCP stands on its own

Decided on 2026-10-17. Since v0.6 the paper had offered SchCP as a contribution to UCP's Local Services council, Wix's USP and AP2: a profile of USP or a capability under the council, and constraint types for AP2. SchCP is now its own protocol for scheduling people, at a business and between people's own calendars. It still borrows where those efforts fit (RFC 9421 signatures, a .well-known discovery document, idempotency keys, AP2's signed limits bound to an agent key) and keeps its envelope close to theirs so that one endpoint could speak both; a bridge is an open question, not a goal. The paper's §0, §4, §11 and §12 were rewritten: the ask is to review the spec, implement it independently, and pilot it, with no venue named. Whitepaper §10.22.

Why: the user's answer to "join or go alone" was to stay independent. The paper gives the reason it sees: what SchCP defines (what the person allowed, and meetings between people) is what those efforts leave out.

8.20 What a second implementation needed: the spec states it

Decided on 2026-11-03. An independent Python implementation written from the spec alone (implementations/python/, from 2026-10-17) passed every vector, but about 75 of its cases were decided only by the vectors or the schema, and it listed 24 findings. Each is now settled where an implementer reads:

Why: the user chose a second implementation as the next proof step (§8.19 and the decisions of 2026-10-17), and asked on 2026-11-03 to fix what it found and run another pass. Implementations that agree only because they copied the vectors would not show the spec is enough.

The second pass (2026-11-03). A fresh agent, with no sight of the reference or the first implementation, wrote it again from the revised spec and opened the vectors only when done: 218 of 233 passed on the first run, and only 8 failures needed a rule found only in the vectors (how to write a computed duration, now in §2.1). From its findings the spec also now says: an agent refuses a waiting move whose deadline is after the start; discovery is fetched unsigned; SchCP-Agent-Key carries only public members (the reference accepted a private d, and now refuses it); a nonce is remembered only after its signature verifies; a passed exp is mandate_expired (the reference said mandate_invalid); and when a request fails several checks the host may report any one of them. Whitepaper v0.15, §10.23.