#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.
- Most of the hard parts of v0.3 were about money. Deposits needed a payment provider, captures, voids, refunds, a payment journal with leases, crash tests that kill the host at each rail call, and a fix for a capture that could land after a void. They also needed binding an authorization to its payer, rules about forfeits, and an answer to the payments-risk reviewer's question of who underwrites a no-show fee. None of that is about arranging time.
- Most meetings have no price. Two parents arranging a playdate, a coffee, a parent-teacher meeting, a sales demo, a viewing: the protocol was making every one of them look like a checkout.
#2. What v0.4 removes
- The payment provider interface, the mock and file-backed providers, deposits (
Service.deposit,deposit_after_no_show,forfeit_deposit,max_deposit),paymenton holds and confirms, refund state,deposit_capturedandfees_chargedon bookings, the payment journal, and the confirm and approval leases. Those leases existed only so a hold could not lapse while money was being captured. - Error codes
DEPOSIT_REQUIRED,PAYMENT_DECLINEDandPAYMENT_UNKNOWN. - Adjustments: recorded amounts added to the next offer for the human to approve. Carrying a debt from
one booking to the next is collecting money with extra steps. The
enforceandlapse_afteroutcome policies and theadjustment_requires_approvalreason go with it. - The crash tests and the money-and-leases tests. They tested a failure class that no longer exists. Confirm is now one database transaction: nothing outside the database happens between checking the offer and writing the booking.
#3. What stays, re-read
- Prices are disclosures. A host may state a price. If it does, the signed offer carries it with
the all-in total and lines, the consent text says it is paid to the host directly, and
max_pricebinds to the total. A host may also state no price, and then nothing about money is in the terms. Fees, surcharges and per-person pricing require a stated price. - Outcomes became facts, then went too. The first v0.4 pass kept v0.3's outcome records (a
no-show, late cancellation or late change, with the stated fee as information) and renamed
if_chargedtoif_no_show_fee. A later decision removed the records and the fee terms entirely; attendance stays as a status (§5). - Closed mandates still exist, because evidence that the human saw the exact terms matters without
money.
if_chargedbecameif_no_show_fee, and then went with the fees (§5): the values are nowneverandalways. - A host that wants deposits takes them outside SchCP, in its own checkout, and SchCP does not vouch for that. The payments-risk seat's question ("who underwrites the deposit?") now has a plain answer: not this protocol.
#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.
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.
It writes one meeting contract (
MeetingClaims, a JWS with typschcp-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.- the title, start, end or
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.
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.
Each member's calendar then waits (
pending_host) for that member's acceptance: a JWS with typschcp-acceptance+jwtnaming the contract's hash. It comes from their own key, or from their agent under a rule they signed (§4.6). Accepted, the booking istentative. The organizer's own calendar istentativefrom the start; its owner signed the contract.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:
- A person can sign a rule for the agent that keeps their calendar: a JWS with typ
schcp-rule+jwt. It names that agent's key (cnf.jkt), the person's own words (label, such as "Weekend playdates with my contacts"), and optionalservices,weekdays(with atz) andorganizers. - When a rule covers a booking, the agent may say yes without asking. The yes names the rule.
- A rule is the person's signature made in advance, so a confirmation always traces to a person.
- Every booking says who answered (
Booking.answer:by: host | person | rule, with the rule's label), so the guest's agent can tell its human "Priya's calendar said yes by her rule; Priya hasn't seen it". - A guest can ask for the person (
ConfirmRequest.answers: "person", or the meeting'sanswers). Then no rule may answer, and a rule acceptance is refused withperson_required. - The SDK leaves that choice to the caller. The spec says hosts SHOULD ask their person, and use rules only where the person chose them.
- In the sample world, Noor's calendar has a rule for playdates and coffee with contacts. Ellis has
none, so Ellis's agent asks every time. Priya has one for weekend playdates. Noor's calendar moved
from
instanttohost_approvalplus that rule, because an unsigned instant yes from a person's calendar is what the user asked us to avoid.
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.
- Ordinary booking. The counter is a host-signed change offer. The guest's person accepts it with the existing proposal response, and that makes the booking final: the counter was the host's yes. Declining leaves the booking waiting for an answer to the original terms.
- Meeting. A counter changes the contract.
finishMeetingreportscountered.reviseMeetingasks every other calendar for a change offer with the new place (change offers now takeplace), writes revision 2 over the new offer hashes, and gets the organizer's human to approve the new text once. It then moves every booking onto it (POST /bookings/{id}/meeting). - Re-answering. The hash changed, so every member answers again; a yes to revision 1 no longer counts. Rules re-answer by themselves; people are asked again. Revisions only go up.
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.
- Fee terms:
Policies.cancellation,reschedule(free | as_cancellation),no_showandoutcomes;OfferClaims.cancellation_policy,no_show,reschedule,outcomesandrecords;Booking.cancellation_policy. - Outcome records:
OutcomeRecord,OutcomeKind,OutcomePolicyandBooking.recorded. - Mandate constraint
max_no_show_fee, and theclosed_mandatevalueif_no_show_fee(nownever | always). Harbourview Dental in the sample world moved toalways. POST /bookings/{id}/cancel-quoteandCancelQuote.- The 60-second undo (
undo_until, and the rule that an undo counts like a released hold). With nothing recorded, an undo has no meaning: every cancellation is already free. - Discovery capabilities
undoandoutcome_records. - The participants' deadline cut-off at the first late-cancellation tier, and the
too_soonrefusal whose basis was "participants must answer before a late cancellation would be recorded". The deadline is now min(now +confirm_within, the start). A change for required participants to a start that has passed is stilltoo_soon.
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.
- Discovery:
merchant→host(with an optionalkind);policies.access;closed_mandateisnever | always(if_chargedandif_no_show_feegone); no deposit fields; nopolicies.cancellation,reschedule,no_showoroutcomes; capabilitiesundoandoutcome_recordsgone. - Mandates: no
max_no_show_fee(constraints are closed, so a mandate carrying it is invalid). - Operator documents:
merchants→hosts,merchant_domain→host_domain. - Holds and confirms: no
payment;service_location→place. - Offers:
merchantclaim →host;slot.endoptional withopen_end;price,totalandlinesoptional; nodeposit;place; nocancellation_policy,no_show,reschedule,outcomesorrecords. - Bookings: no
deposit_captured,fees_chargedorrefund; nocancellation_policy,recordedorundo_until;endoptional withopen_end;cancelled_byisguest|host. - Statuses and events:
pending_merchant→pending_host;booking.cancelled_by_merchant→booking.cancelled_by_host; newbooking.ended; webhooks carryhost_id. - Errors: payment codes removed;
NOT_PERMITTEDadded;merchant_*reasons →host_*;*_customer*reasons →*_guest*;location_required→place_required. - Participants: role
guest→invitee;payerremoved. - Cancel quote:
POST /bookings/{id}/cancel-quoteandCancelQuoteremoved (the first v0.4 pass had only dropped itscharge). Cancelling has no undo window and records nothing. - Participants' deadlines: no longer cut off at the first late-cancellation tier; confirming with
required participants close to the start is no longer
too_soon. - Meetings and answers (this pass, no v0.3 equivalent):
ConfirmRequest.meetingandanswers;CommitRequest,MeetingRevisionRequestand their routes;MeetingClaims,AcceptanceClaimsandRuleClaims;Booking.answerandBooking.meeting;BookingResponse.contract;OfferClaims.host.owner;Proposal.place, andChangeOfferRequestwithplaceand optionalstart;- webhook
booking.accepted; - reasons
meeting_invalid,acceptance_invalid,person_required,awaiting_membersandmeeting_incomplete.
- From live testing (§8):
Proposal.awaiting_hostfor a guest's move waiting for the host, with how the guest agreed to it (approved_byat first,consentsince 2026-10-17);Booking.declined_change;- webhooks
booking.change_approvedandbooking.change_declined(withrefusal); MeetingMember.answers;OfferClaims.resource_name;- reason
window_too_shortfor a waitlist window shorter than the appointment.
- JSON Schema definitions for what a host returns about a booking, which had been typed only in
the code:
Booking,Proposal,Participant,HostAnswer,MeetingState,JSCalendarEventandReceiptClaims. All are closed except the JSCalendar event;WebhookEvent.bookingrefers toBooking, and the agent SDK refuses a receipt whose claims failReceiptClaims. - Closed definitions for the rest of what a host returns:
Hold(which always carries its offer),WaitlistEntryandBookingResponse.WebhookEvent.holdand.waitlistrefer to them, and the agent SDK refuses a hold, waitlist entry or booking response that fails, naming the field. - Records and receipts (§8.11, §8.12):
policies.retention(required) in discovery andretention(required) in every offer and receipt;ReceiptClaims.approved_bybecameconsent: { approved: "offer" | "contract" | "limits", mandate, mode? }, andProposal.approved_bybecameProposal.consent, the same shape.
#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
- A member may take back a yes until the meeting is final. Round 11: Priya's weekend rule said
yes to a playdate, Priya then said she could not make it, and her calendar's decline was refused
because the booking was already
tentative. A rule answers for a person; it does not bind them against their own word. Nothing is final before commit, so nobody is held to a yes until then. Round 12: a decline is recorded as the host's own answer, because rules only ever say yes (§8.9). - A member who backs out is quoted to the others. Round 22: Priya cancelled her part of a coffee before it was final, and Ellis's calendar heard only "Priya Shah's calendar is no longer in it". A no already carried the person's words, to the organizer and to the other calendars. Backing out is the same act seen from the other calendars, so they now hear "Priya Shah backed out" with her message. Round 9 settled who is blamed: a no or a lapse is the cause before any cancellation, because the organizer's own calendar is cancelled as a consequence, and the verdict is kept once found. Spec §5.12.
#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:
consent.approved:"offer"(the person's key app signed this exact offer's hash, a closed mandate),"contract"(they signed a meeting contract naming it) or"limits"(their agent booked within limits they signed). It replacesapproved_by: "principal" | "agent", which could not say that a meeting's organizer signed the contract. A host-side rule is not here: who said yes for the host is the booking'sanswer(§7.9);consent.mandate: thejtiof the mandate the person signed, the root of the chain. Links an agent adds when delegating are the agent's own. After a move, it is the mandate that accepted the move;consent.mode:human_presentorhuman_not_present, as that mandate'sconsent.modesays;retention: the contract's own.
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:
- Request signing (§3.2): the label
schcp, one order of components and parameters, the signature base line by line, the nonce,SchCP-Agent-Key, an empty body as bodiless; hosts refuse any other form.stale_signatureandreplayed_nonceare now the reasons the reference sends (it sentbad_signaturefor both), and the conformance runner checks them. - Child mandates (§3.3):
issuer_jwk,parent, the sameissandsub, no laterexp, each check per link, root first. One stage for a wider child: its constraints are not compared with the parent's, every link binds every action, so a wider limit is refused at the action (403), never at verification. The vector's alternative outcome is gone. The reference now also refuses a child that changessub. - Money (§2.1): ISO 4217 minor units, no extra decimals, canonical form, exact percentages
rounded half up, no arithmetic across currencies. The reference's
percentOfnow uses integer arithmetic on the percentage as written instead of a float product. - Schema use (§2.1, §3.3):
formatasserted; ECMA-262$; closed at every depth. Nested objects of signed claims (host,service,slot,reconfirm, price lines,cnf,consent, meeting members, the JSCalendar event,Refusal,Remedy,Jwk) are now closed in the schema, so a fee insideslotfails like one at the top (new vectornested-fee-term).Refusalgained thedetailsandalternativesthe reference already sent. Discovery documents stay open and unsigned. A newRetentiontype allows weeks and days only. - Keys (§2.1, §3.4): host-signed JWS carry the
kidof their discovery key and verifiers use only that key (the reference client tried every key; an engine key made without a kid is named by its thumbprint). P-256 is for the SD-JWT profile only. - Smaller rules: duplicate JSON member names are refused (the reference now checks bodies, JWS
and
SchCP-Agent-Key); price, total and lines come together and the lines sum to the total (the reference client now checks); a closed mandate may read but not release; a table of what each stage of a guest's move shows (§5.7), including that a declined move does not bumpsequence; acceptance and rule timing as inequalities; inclusive windows; exact hostnames in §2.2; the receipt's two contract hashes agree;SchCP-Versionon served discovery documents (the sample site's copies lacked it). - The sample site:
GET /api/dnsand the start-up log give each simulated domain's local port, so a client written elsewhere can reach the hosts.
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.