Concepts
A typed inbox
Section titled “A typed inbox”Everything that arrives becomes an item of one of five types: message, quote_request, booking, order or refund. An item has a typed payload (field names follow schema.org where they fit: reservationFor, orderedItem, totalPrice), a state, a version number, the party it belongs to, the channel it came through, and three flags: needsHuman, priority (0 to 3) and sandbox.
Each type has a state machine, defined as data. A transition names the event, who may fire it (customers, the owner and staff, the owner’s AI, a rule, a connector, the system), the states it applies from, the state it leads to, optional guards, and the effects it triggers.
| Type | States |
|---|---|
| booking | requested, needs_info, proposed, confirmed, completed, no_show, cancelled_by_customer, cancelled_by_business, declined, expired |
| order | received, needs_info, proposed, accepted, awaiting_payment, payment_failed, paid, fulfilling, fulfilled, completed, declined, cancelled, charged_back, expired |
| quote_request | received, needs_info, quoted, accepted, declined, expired |
| message | open, answered, closed, spam |
| refund | requested, approved, goods_received, refunded, rejected, cancelled |
A booking, for example, moves from requested to confirmed when the owner fires confirm (guard: the slot is still free; effects: claim the slot, issue a receipt, notify the customer), or to proposed when the owner fires propose with another time. The customer then answers, through their assistant or the links in the business’s email: accept books that time, cancel declines it and closes the request, and counter asks for another free time, which puts the request back with the owner. When the customer said yes by phone or in person, the owner’s confirm on a proposed booking books the proposed time; only a person may do that, not the owner’s AI, a rule or an integration key. The owner can also confirm a booking that was waiting on the customer’s details (needs_info), and a customer’s details, however they send them, move it back to requested; an automatic reply from their mailbox (an out-of-office, a returned email) is kept on the item as a note and moves nothing, and one that answers no item is kept as a notice, for the owner alone: none is ever answered. Nothing is booked, proposed or accepted for a time that has already started, and free times are offered only from the business’s minimum notice on (an hour unless the owner sets another, in Settings → General): inside it, a customer’s request is taken and waits for a person (a rule that would confirm it flags it for one instead), and no customer, rule, AI or key books the time, while the owner or staff in the app still may; nobody proposes or quotes a time inside it, since the customer could not accept it in time. Whoever reads an item is told which events it accepts right now, so the valid next actions are never a guess: for the owner they become the primary buttons, for an agent they are listed in the item view.
A quote works the same way: the owner’s quote (again, to replace one already sent) must add up, and a quote that creates a booking names the time. When the customer accepts it, what it was for is created in the same write, already promised: the booking confirmed at the quoted time, or the order accepted. An order can be answered the same way too: the owner’s propose suggests changes (another quantity, a price of their own, a delivery date), which the customer accepts, declines (closing the order) or answers with quantities or a date of their own. When a customer asks the business, by email or phone, to cancel, the owner records it with record_cancel and the customer’s words: it is the customer’s cancellation, judged late or not at the moment they asked, never the business’s.
Offers. Everything the two sides put to each other before a promise is an offer: the customer’s request is the first, and each time the business proposes, quotes or suggests changes, or the customer answers with a time, quantities or a date of their own (make_offer), the open one is closed (countered, or superseded when its own side replaces it) and the next opens, one round further. Each is kept as an immutable snapshot of its terms with their fingerprint (terms_sha), who made it, until when it holds, what changed from the one before, and what the other side was shown; accepting one (accepted) is the promise, and the item’s payload points at it (offer). There is at most one open offer per item, and every verb happens once, in the same write as the item’s move. The business sets its prices: a price of the customer’s own goes to a person at the business as their message, never refused, and so does a suggestion after negotiation.maxRounds rounds. The owner’s AI and rules change times, quantities and delivery, never a price, and propose nothing more after the last round. With negotiation.binding off, what the business proposes says it is subject to its confirmation, and retract withdraws it before the customer answers; the customer’s request comes back to the owner. The owner sees every offer of an item (list_offers).
Changes to what was agreed. A confirmed booking or an accepted order can change without being cancelled. Either side asks (propose_change: the business names another time, a price of the owner’s, other lines or a delivery date; the customer another time, other quantities or a delivery date, never a price), and the other side answers: accept_change moves the promise in the same write — its time and its places, or its lines and total — while decline_change, the asker’s retract_change and a lapse (expire_change) leave it exactly as agreed. The customer hears it in the business’s words (“Can we move your booking from Tue 10:00 to Wed 10:00? If that doesn’t suit you, your booking stays as it is.”), with links to accept or to keep it as it is, and every confirmation carries a link to ask for another time. A customer’s time must be one the business would offer (its own places count as free to it), and a change the business asks for is held for them like a proposed time. A change lapses at its date: one the customer asked for after negotiation.counterValidHours, and before either start; one the business asked for at the date it gives, else after negotiation.offerValidHours for an order, and for a booking never after either start less the minimum notice; one the owner’s AI or a rule asked for after negotiation.offerValidHours at the latest. Each promise takes at most negotiation.changes.maxPerItem changes (three at most), none moving its date, or a booking’s end, more than 90 days from what was first agreed; past that, or past the last round, a customer’s change goes to a person as their message, never refused. The owner’s AI and rules accept a customer’s change only when negotiation.ai.mayAcceptChanges allows it, before negotiation.changes.customerCutoffMin (else the cancellation window) and at the business’s prices, and ask for one only when negotiation.ai.mayProposeChanges does, never at another price. Once payment was asked for or made, a change keeps the order’s total. A change both sides agreed is a signed receipt of its own (an amendment), which a network reads from its rules version 6 on: while a network that holds the promise, or is switched on to be sent it, does not yet have version 6 in force, it would hold the business to the date first agreed, so that promise does not change: the customer’s request goes to a person, and the owner hears the way to do it (the customer’s own cancellation and a new booking). With no network on, a change is recorded at once.
The confirm step. A booking or an order that carries a price binds a consumer only once they confirmed its summary — what, when, from whom, the total, the right to withdraw or why there is none, and that confirming means paying — as the law asks before an order to pay. Their assistant is shown the summary first (409 confirm_terms, or on MCP a result that says “Nothing is sent yet”), and sends the request again with its fingerprint on their yes; nothing is written before. A business that sells only to businesses (commerce.customers) is not asked for it, nor is a request that holds no price of the business’s yet, or a free one.
Returns and withdrawal. A customer may withdraw from a contract made at a distance within 14 days (or the business’s longer returns.days), without a reason: from an order, from its acceptance until 14 days after the goods reached them, and from a booking they paid for, until it starts. The period is counted as the law counts it — the first day not counted, an end on a weekend or a public holiday moved to the next working day, in the business’s time zone — and nothing a product or service is excepted for (made to order, perishable, for a set date, …; its withdrawal flag, the owner’s to set) may be withdrawn from. Every confirmation of such a contract carries the right, the model form, who the business is, and a Withdraw from contract here link, which shows the statement and sends it on Confirm withdrawal; the customer’s assistant does the same in two steps (withdraw_from_contract), and a customer’s cancel of something they paid for is their withdrawal while the right runs. Before the goods went out a withdrawal ends the order or the booking, neutrally, and what was paid is refunded within 14 days; after, the goods come back and the refund follows them. It is never refused: past the period it becomes a return under the business’s own policy, or the customer’s message for a person. A return is an item of type refund, linked to the order and the same customer’s: a withdrawal, faulty goods under the legal guarantee, a return under the business’s policy, or a paid order the business cancelled. The business approves it (with the goods coming back by a date, or not), marks the goods back, and refunds; only a person refuses one (never a withdrawal), disputes what came back, or cancels a paid order, and a refund of less than what is owed needs the customer’s agreement. The owner’s AI and rules may approve a return inside the owner’s policy with the goods coming back (negotiation.ai.mayAuthorizeReturnsInPolicy), never pay one. A refund is due within 14 days of a withdrawal’s notice (or three days after the goods come back, when later), else returns.refundDays after nothing more has to come back; the owner is told before it falls due. Returning something never counts in what the business knows of a customer, and a charge-back while a return is open counts against nobody. A refund of a customer’s order is a promise of the business’s own, with its own receipts: kept when paid by its date, broken when paid after it.
Nothing waits forever. A request nobody answered lapses (expired): a booking after booking.autoExpireHours and at its start at the latest, an order or a quote request after negotiation.counterValidHours, counted from when it came in or the customer last answered or was asked something. What the business proposed lapses at its deadline: a time at its start less the minimum notice, a quote at the date it gives, changes to an order at theirs, anything without a date after negotiation.offerValidHours, and whatever the owner’s AI or a rule proposed after that at the latest. The deadline is checked in the write that would accept it, and the sweep closes what lapsed and tells the customer, in the business’s words. With booking.holdOnPropose on, a time the business proposes is held for the customer until they answer, at most booking.maxHolds at once per customer.
What the customer hears. Every email to a customer is the business speaking, in its name and in the customer’s language (English or Portuguese: the language their assistant gave, else the business’s first language, which the setup asks for). A booking, order or quote request is acknowledged a moment after it arrives, unless the business, or a rule, answers first; then every change the customer needs to know of gets an email with the time in the business’s time zone and the zone named, the price and the total, until when to answer, the links to answer with, and a six-character reference, never an id. A reply to any of them lands on the same item. An email no person typed — a rule’s, the owner’s AI’s, another system’s (an integration key, unless its request says a person wrote it) — says so in one line, “This reply was sent automatically. Reply to reach a person.”, in the business’s words. Each email is kept with what became of it: the owner sees on the item whether it was sent, is still being tried, failed, or was not sent and why, and an email to the customer that failed for good marks the item as needing a person. A customer who came through an assistant reads the business’s replies at the status door.
A confirmed booking and an accepted order are promises, and every way out of one records how it ended as a signed receipt: completed, a no-show, cancelled by either side (a withdrawal is the customer’s own, never late), fulfilled, not fulfilled, a failed payment, a charge-back. A change both sides agreed to one is a receipt too, and the outcome then names the new dates; a refund owed is a promise of its own, paid on time or late. Some of it happens because time passed: a confirmed booking counts as completed 48 hours after it ended unless it was marked a no-show (booking.autoCompleteHours), and an order whose payment never came lapses 14 days after it was requested (orders.payDays), which closes it for the networks and leaves it open for you. A customer who cancels after your cancellation window is recorded as cancelling late, or refused, as booking.lateCancellation says. Receipts has the full list.
Events, versions, idempotency
Section titled “Events, versions, idempotency”Every transition is an event with an actor, a timestamp, a reason and a diff, appended to the item’s history. The event sequence number doubles as the version: a write that carries expected_version fails with version_conflict if someone else moved the item first.
Every mutation from an agent or an API client carries an idempotency key. The same key with the same request returns the same answer and creates nothing new; the same key with a different request is refused with idempotency_mismatch. Correctness lives in the database: unique constraints, idempotency rows and slot claims are written in one batch, so races are settled by SQLite rather than by application code.
Rules are plain JSON, evaluated deterministically after each committed event. A rule has triggers (item.created, item.transitioned, item.transitioned:<event>, thread.inbound), a condition and actions.
Conditions combine all, any and not with comparisons on paths in the item (eq, neq, lt, lte, gt, gte, in, nin, contains, startsWith, exists, empty, between) and a few named checks: slot_is_free, within_business_hours, party_verified, text_has_keywords, is_sandbox. Actions are transition, set_flags, reply (to the customer, or an internal note), enqueue a job, or stop.
A rule can also read who is asking: person (the best tier and score across the networks that presented the customer, each network’s own, and limit_minor, 40000 for a trusted person), customer (match — strong, weak or none — and your own history with them: completed, paid, no_shows, late_cancellations, payment_failed, charged_back, largest_paid, open_bookings, first_seen) and agent (level: vouched, self or none, and platform), with the checks person_trusted, person_tier_on {network, min}, customer_known (a customer you know with a completed visit or order) and within_customer_limit (within twice their largest paid order, or a trusted person’s limit). Positive only: a rule that reads any of these, or party_verified or the trust tier, may speed things up or ask a person — confirm, accept, ask for payment, flag, reply — and never decline, cancel, expire, mark a no-show or queue a job. Saving one that would is refused with positive_only; an older one has those actions skipped, and the rule’s run, the item’s timeline and “Try it” say so in plain words. agent.level is vouched only for a platform a network you report to recognises; any other signed agent is self.
Nothing in a condition does I/O; everything a rule may read is fetched into a frozen context first. Limits keep rules honest: at most 20 rules per event, a chain depth of 3, and a per-rule cap on runs per item.
A rule reads your price, and promises only on it. A booking’s or an order’s totalPrice is the one your catalogue gives; a different figure the customer’s request carried is kept as customerStatedPrice, which no condition can read. A request that holds a price your catalogue does not give (an order line naming no product you sell, a booking of a service priced from, by quote or not at all that names a price, or a fixed-price service booked for longer than it lasts) is never confirmed or accepted by a rule: a rule that tries flags it for a person instead, and the rule’s run and “Try it” say so.
Your AI does time, and money only within your limits. The AI you connect to the owner’s MCP is held the same way, and further: it never confirms or accepts a request that holds a price you did not set, never records a payment or a refund, never changes a price or your currency, and never connects or removes a feed (a feed sets prices). What it adds with a price is saved unpublished for you to check and publish. What it offers — a time, a quote, changes to an order — is judged against your limits (below), and so is what a rule or another system’s key without money:write offers. When money is involved it leaves you a note with what it suggests.
Your limits, in code. What automation may agree to on its own is yours to set (negotiation.ai), and it is enforced in the write, not asked of a prompt: out of the box, time yes, money no — any free time within seven days of the one asked for, no discount, no price the catalogue does not give, no later delivery, no change asked of the customer, no refund with nothing sent back. Inside them an offer goes to the customer as it would from you. Outside them it is not sent: it is kept as the item’s draft for you to send as it is, change or drop, with the limits it is outside named as codes; an acceptance outside them is refused, and a reply naming an amount of money you have not offered is kept as a note. Automation learns a limit as a code when it meets it, never as a number, and never reads your lowest prices (floors, per product or service), your limits or your rewards, so no customer’s message can get it to recite one. You in person are never held.
Prices of the customer’s own. Out of the box you set your prices: a price the customer suggests reaches a person as their message, and what you proposed stands. Switched on (negotiation.priceCounters), it is their answer to your offer, for you to take or answer; automation takes one only at or above your floor, and never answers a price with another, since a floor revealed one step at a time is a floor given away. What you mark not to be haggled, a quote, and a customer who suggests prices too often go to a person, never refused.
Rewards. A good record may earn a better price, set by you and applied by the inbox: a condition on what the customer earned — your own history with them, the standing their assistant showed — and a percentage off the list price, never below your floor and never above the list price, the best one that matches and never several. A reward never reads who or where someone is. Whenever a price was chosen for the customer by a reward or by automation, everything that shows it to them says so, beside the list price.
Prices per person. A service’s fixed price is for the whole booking unless you set it per person, in which case a booking for four costs four times the price.
Presets exist for three verticals. Appointments: confirm at once a customer you know (two completed, no no-show) or a trusted one (at most two open bookings, up to 200.00), otherwise auto-confirm bookings under a limit when the slot is free and inside opening hours, and ask a person about everything else. Trades: quotes always need a person, urgent words raise priority, trusted customers and customers you know go first. Shop: accept at once an order within a customer’s limit, accept small orders and ask a new customer to pay, flag large ones for approval. All three offer a one-time code to someone who gives a known customer’s address and asks about earlier items. A new customer’s booking is never refused for being new: at a time you offer, it waits for a person.
Agent policy and trust tiers
Section titled “Agent policy and trust tiers”Every request is resolved into a caller with an actor, a channel and a trust tier. The tiers are:
anonymous: nobody proved anything. Allowed to send messages and to request quotes, bookings and orders. On creation the caller receives anaccess_token, a capability secret that is the only way to read or cancel that item later.signed_agent: the request carried a verified HTTP Message Signature (sdi-agent/1: the agent’s own Ed25519 key, or a platform’s key under Web Bot Auth). A signature that does not verify counts as none, and the answer says why inSdi-Signature.verified_principal: the caller holds an API key issued by the instance, an OAuth token, or a session; email that arrived through a path that verified DKIM and SPF is treated the same way.reputed_principal: an agent a platform vouches for, carrying a person a network trusts.
People and customers you already know. An agent may carry its person’s pass (sdpass1_…), issued by a network; the inbox presents it to that network, which says how that person keeps their promises, and a person the business has met before is recognised as the same customer. A first booking or order with an email and nothing carried gets the customer a key from each network switched on, and the agent their first pass; the key reaches the customer a day later in a short email of its own, with one line about the booking network and a link to a page on the instance that explains it, where the customer can switch it off for themselves, and never rides on another email. From then on no network hears anything more about them, and the owner sees that they did. Someone who only gives the address of a customer you know is a weak match: their item stays on its own, you see “may be Ana Silva, unconfirmed”, and a one-time code sent to the known address makes them the same customer. The agent’s side of all this is the agent guide.
The manifest publishes which tiers an instance serves. A tier or a record only ever speeds things up: nobody is refused for being unknown or unsigned, and a new customer’s booking at a time you offer waits for a person’s confirmation.
Test mode. An instance in test mode marks every item as sandbox. Any caller can also ask for a sandbox item with the x-sandbox: 1 header or by calling a sandbox. hostname. Sandbox items go through the same machines, but nothing leaves the inbox: no email to the customer or to you (each is kept as not sent, so you can read what would have gone), nothing asked of or shown to a network, no receipt. While test mode is on this holds for every request, a real customer’s too, and the inbox says so above the list.
Networks
Section titled “Networks”An instance can report to several networks, and each one lists it in its own directory. They are the networks setting, a map keyed by each network’s https origin: {"networks": {"https://network.surfingdog.ai": {"enabled": true}}}. Settings are merged, never replaced, so adding one network leaves the others as they are, and switching one off is "enabled": false (it stays in the list, and anything it missed is sent when it is switched on again). At most eight. Each entry can limit what that network gets, with share: {"counts": false} or share: {"receipts": false}, and whether it gives first-time customers a key, with issue: false; everything is on by default. A fresh instance lists https://network.surfingdog.ai, switched off. Any service that implements the network protocol works. Its rules say what it offers: at the directory level it lists the business and keeps its receipts, with no standing to show and no keys to give, so the instance never asks it for either and Settings → Networks says so; at the full level it does both. To run one, start from the example network and check it with network-check.
For each network that is on, the instance registers its domain, which the network verifies by fetching the manifest (until it has, the instance asks again at most once a day); then every hour it sends its software version, its runtime and the number of bookings, orders, quotes and messages created in the last 24 hours, and it publishes every receipt it issues (below), which names the customer only by a pseudonym. With INBOX_SECRET_KEY set the hourly ping is signed, and the network answers it with the business’s own standing there — its tier and score — which Settings → Networks shows. Each network is called on its own, so one that is slow or down never holds up another. An older document’s network: {url, join} is read as one entry of this map. The numbers on the front page of surfingdog.ai are the sum of the pings network.surfingdog.ai receives.
A network also keeps a record for people. A customer’s assistant presents their pass; the network that issued it answers with how that person keeps their bookings and orders (new, building or trusted, what they kept and broke, since when), and the owner sees it on the item, one line per network, only for people whose assistant presented a pass. A customer’s first booking or order with an email, from an assistant carrying nothing, gets them a key from each network with issue on: the instance sends that network the email address, and the network keeps a keyed hash of it. The network emails the key to the customer itself, in the business’s name, once the business’s receipt names them, so the business never holds it; a network that leaves that email to the business sends the key to the instance, which emails it a day later, alone, with a link to the page that names the network, says what it keeps, and stops it for them with one button: after that, nothing more about them reaches any network, and what a network already has is listed for the owner (a network cannot yet be asked to erase it). The privacy page lists everything that crosses.
Receipts
Section titled “Receipts”When a booking or an order becomes a promise (a booking confirmed, an order accepted or paid) the instance issues a receipt: a compact JWS signed with the instance’s Ed25519 key, which the manifest publishes in receipt_keys and /.well-known/jwks.json serves as a plain JWKS. When the promise closes it issues another, the outcome: completed, a no-show, cancelled by either side, fulfilled, and so on. A change both sides agreed in between is an amendment, and a refund has a promise and an outcome of its own; networks read these from their rules version 6 on, announced fifteen days before it counts (at once while a network has no other member), and are sent them only then. The receipts are on the item for whoever may read it, and the customer’s agent counter-signs them with acknowledge_receipt. Both sides then hold a small, verifiable proof that this transaction happened between these two parties and how it ended, without either holding the other’s address: the receipt names the customer by a pseudonym only this instance can produce. A network counts a promise when it closes, kept or broken, for the business and for the customer, and weighs what a verified agent counter-signed above what the business says alone. The format and the rules are on the Receipts page; how a network scores and orders is its published rules (GET /v1/ranking on the network; for ours, the network page).
What a network measures is promise against delivery, not taste: every promise is kept, broken or still open. Reviews of taste may come later, only ever against a co-signed receipt; ADR-012 explains the legal reasoning.