Competition conformance
Normative.
Everything else in this standard is optional for a vendor who only wants interoperability. This file is what a simulator must implement to host a sanctioned SGA event, and it is not negotiable.
A bay where the weather is different, the assists are on, or the swing came from a mouse is not playing the same competition as everyone else.
The design goal is to remove every variable that moves a score and is not the player.
1. The Event Rules Package#
A signed document issued by SGA for an event, distributed to every participating bay before play. Our "digital rules package," made machine-enforceable.
{
"event_id": "sga:event:01J9X4...",
"ruleset_version": "SGA-CR-2.4",
"bundle_version": "2026.09.1",
"issued_at": "2026-09-20T00:00:00Z",
"expires_at": "2026-10-04T01:00:00Z",
"signature": "...",
"course": {
"digest": "sha256:c41d...",
"version": "1.4.2",
"source_url": "https://<rights holder>/courses/pebble/1.4.2",
"tee_set": "championship"
},
"conditions": {
"mode": "fixed",
"wind_speed_mps": 3.1,
"wind_direction_deg": 215,
"green_speed_stimp": 11.0,
"firmness": "firm",
"temperature_c": 20.0,
"pressure_hpa": 1013.25,
"pin_positions": { "1": "back_left", "2": "center" }
},
"play_rules": {
"tee_time_window": ["2026-10-03T13:00:00Z", "2026-10-04T01:00:00Z"]
},
"assists": {
"prohibited": [
"auto_putt", "gimme", "mulligan", "putt_preview", "aim_assist",
"shot_shaping_assist", "club_recommendation", "green_reading",
"elevation_auto_compensation", "wind_auto_compensation", "shot_replay"
],
"required": [ { "id": "max_strokes_per_hole", "count": 10 } ]
},
"input_requirements": {
"swing_input": "launch_monitor_only",
"prohibited_inputs": ["mouse", "keyboard", "gamepad", "touch", "simulated"]
},
"bay_requirements": {
"min_certification_tier": "diamond"
},
"device_requirements": {
"launch_monitor_required": true,
"min_attestation_tier": "A2",
"certified_models_only": true,
"firmware_digest_allowlist": ["sha256:...", "sha256:..."],
"required_fields": ["ball.speed_mps", "ball.launch_angle_deg", "ball.spin_rpm"]
},
"software_requirements": {
"min_vendor_assurance": "certified",
"settings_lockout_required": true
},
"identity_requirements": {
"assurance_level": "verified",
"presence": ["proximity_token", "shot_cadence"]
}
}assists is stated in the vocabulary every round declares its settings in (03 §3.4g), so what an event prohibits or requires and what a round has in force are compared id for id. A gimme distance, a mulligan allowance and a stroke cap are assists with a parameter rather than separate rules. So holing everything out is gimme prohibited. A ten-stroke cap is max_strokes_per_hole required with a count of
- A format that resolves putts automatically requires
auto_putt.
software_requirements is to the simulator what device_requirements is to the hardware, and it is a separate block for the same reason the other two are separate: it asks a different question of a different participant. min_vendor_assurance is the §2.1 axis: is a named party accountable for what this software reports. It is not interchangeable with min_attestation_tier, which asks a different question — whether a participant can prove that what it sent came from it.
A simulator can reach any tier (02 §2), so the two are not a hardware ladder and a software one. They are different questions about the same participant: a vendor whose key is in a hardware token still signs whatever its software decides to say. An event that expresses its requirement only as an attestation tier has asked about forgery and said nothing about honesty.
Bay requirements are not device requirements#
device_requirements describes hardware and is checked against what is connected. bay_requirements describes the room, and is checked against a signed certificate SGA issued (14).
The two are not interchangeable. Most of what makes a bay certified cannot be observed from the bay at all: the calibration records were reviewed by a person, the installation was inspected, the facility's history was considered. min_attestation_tier: "A2" says the equipment can prove itself. min_certification_tier: "diamond" says SGA looked at this room and signed off. An event that needs the second and asks only for the first will accept a bay nobody ever audited.
Both are enforced at round start, and a bay that fails either does not host the round. That refusal is the point: it happens before the first stroke, which is the only moment at which refusing costs a competitor nothing (14 §1.2).
conditions.mode is fixed (one setting for the whole event) or per_hole (a map keyed by hole). Both were asked for; both are supported. A per_hole package simply carries the same keys nested under hole numbers.
1.1 The engine verifies the package before delivering it#
SGA signs every Event Rules Package, and the SGA Engine verifies it before it reaches a simulator. A package is signed the way a standards bundle is (04 §3): with an SGA key, verified against the pinned SGA root key built into the engine, under the same key rotation. The signature covers the whole package, canonicalized with the signature member omitted (02 §8.5), so no part of it (course, conditions, assists, requirements, validity) can change without the signature failing. The package_digest in request.apply_rules is the digest of that same canonical form.
A signed package carries what ties it to one event, so it cannot be replayed anywhere else or kept past its time:
event_id: the event it governs;bundle_version: the standards bundle it was issued against;issued_atandexpires_at: its validity window.
The engine MUST verify the package before it sends request.apply_rules, and MUST NOT deliver a package that:
- is unsigned, or whose signature does not verify against the pinned SGA root key;
- is outside its validity window, judged by the engine's time anchor (§2.2);
- names an event other than the one the bay is running, or a bundle version other than the one the engine has loaded.
The engine records the package digest and the result of its check in the journal, whether or not it delivered the package. A package the engine refuses is never delivered, so the round cannot be hosted as a sanctioned round: the pre-round block of §2.1, applied to the package itself.
A simulator MAY verify the package as well, and is not required to. Its obligations are to accept request.apply_rules only from the SGA Engine, apply what it can, answer with request.apply_rules_ack, and report what is actually in force in round.settings (§2). A simulator that does verify, and finds the signature bad, refuses with signature_invalid. Verification is required at the engine because the engine already holds the pinned root key and verifies bundles with it; it is one check in one place, the same for every simulator.
Neither check decides whether the round was played under the event's rules. Verification keeps a forged, stale or misdirected package off the bay. The authoritative check is a comparison: the engine compares the simulator's round.settings with the package and journals the conformance vector (§2), and the cloud compares that against the package SGA signed for the event.
2. Push is not enough — the simulator must attest#
The important mechanism, and the one most worth spelling out in full.
Distributing settings to a bay does not make them true. A simulator can accept a package and still have something else in force — a mapping bug, a setting its controls round to a different step, a stale profile, a later change. So the flow is a round trip, and its return leg is not a message invented for events: it is the round.settings every simulator publishes in every round (03 §3.4g).
engine simulator
│ │
verify SGA's signature, the event, the bundle
version and the validity window (§1.1)
│ │
│──── request.apply_rules (signed package) ───►│
│ │ applies what it can
│◄─── request.apply_rules_ack ─────────────────│
│ accepted or refused, and why │
│ │
│◄─── round.settings (changed_by rules_package)│ what is actually in force
compare the package with the declaration
sign the comparison into the journal
│ │
│◄─── round.settings (every later version) ────│ for the whole roundThe acknowledgment is mandatory, and it answers one question: was the package taken up? Most requests need no explicit answer because their effect is visible anyway — a roster changes, a card arrives (03 §1.2). Applying rules answers nothing on its own: a simulator that never received the package, one that refused it and one still loading a course are indistinguishable from outside. So a simulator MUST answer request.apply_rules with request.apply_rules_ack, naming the request_id it answers, with a disposition of accepted or refused. A refusal carries a registered reason — signature_invalid, package_unsupported, package_expired or not_ready — and not_ready is the only one worth retrying. signature_invalid is for a simulator that verifies the package itself; the engine has already verified it before delivery (§1.1).
What an accepted package put in force is not in the acknowledgment. It is in the round.settings the simulator publishes next, with changed_by: rules_package and the package's request_id, reporting the value actually in force — including settings it applied at a different value because its own controls only move in larger steps. A simulator set to green speed 11.0 that only does half steps declares 11.5, and that is not a failure: it produces a clean divergent result a committee can read. Declaring 11.0 while running 11.5 is the one outcome worth designing against.
The simulator reports; it does not grade. It says what is in force and nothing about whether that is what was asked for. The engine compares, key by key:
| in the package | conforming | divergent | unsupported |
|---|---|---|---|
| a condition, the course, an input | declared with the required value | declared with a different value | not declared |
| a prohibited assist | absent from a complete declaration | declared active | absent from a partial declaration — absence cannot be shown |
| a required assist | declared active with the required parameter | absent from a complete declaration, or active with a different parameter | absent from a partial declaration |
A round with no round.settings at all is unattested, and unattested cannot host a sanctioned event. That is the requirement that makes this real — and because the declaration is the same one every casual round already carries, it is not a separate thing to build for tournaments.
A signature is the whole of the authority. Unlike every other request in 03 §1.1, request.apply_rules is not permitted by the asker's role: it is permitted by SGA's signature on the package, and the engine delivers only a package whose signature it has verified (§1.1). A simulator accepts request.apply_rules only from the SGA Engine. It MAY verify the signature itself and refuse with signature_invalid; it is not required to.
The engine writes its comparison into the journal as a signed conformance vector:
{
"rules_applied": {
"conforming": ["course.digest", "conditions.wind_speed_mps", "assists.aim_assist", "..."],
"divergent": [
{ "key": "conditions.green_speed_stimp", "required": 11.0, "applied": 11.5 }
],
"unsupported": ["conditions.pin_positions"],
"verdict": "divergent"
}
}Three verdicts: conforming, divergent, unsupported — and unattested for a round that declared nothing to compare.
2.1 During the round#
- Any settings change mid-round is a new version of
round.settings, carrying who made it and, while a package is in force, that package'srequest_id— so a divergence is read against what was required without re-deriving it. Before and after are the version that was in force and the one that replaced it; the engine compares each new version with the package exactly as it compared the first. This is the highest-signal change in the whole system, and it is usually entirely innocent. - Mid-round divergence flags; it does not halt play. Stopping a round on a settings change penalizes the ordinary case (a crash, a reconnect, a vendor applying a patch) and does not prevent a deliberate one, which is already recorded.
- Pre-round divergence blocks. Before the first stroke is the only moment when refusing costs nobody a round, so that is where the hard gate goes.
Block before, flag during. That asymmetry should hold everywhere in the platform.
2.1a Better than reporting a change: refusing to make one#
Reporting tells a committee something went wrong after it did. Preventing means it did not. Where a simulator is able to, holding the pinned settings for the duration of the round is strictly better than reporting that they moved, and an event may require it with settings_lockout_required.
For every setting the package pins, a simulator does one of two things for the whole round:
- limits changes to the values the package allows (preferred), or
- reports every change to the engine as it happens, as a new version of
round.settings.
The requirement is narrow on purpose:
- Only the keys the package pins are held. Not the whole settings surface. An operator still needs to fix a display mode, a sound device or a stuck window mid-round, and a simulator that freezes everything gets switched off or uninstalled — which protects nothing.
- The lock MUST be declared, and so MUST its gaps. A simulator reports
settings_lockoutin its feature block, and lists inround.settings.lockedwhich pinned settings it is actually holding for the round. A guarantee nobody can see is not a guarantee, and the engine has no way to infer one. - Keys that cannot be held fall back to reporting. Some settings are read from a configuration file at hole load, or live outside the simulator entirely. Declaring a key unlockable is a perfectly good answer and is far better than claiming a lock that does not hold. A change to one of those is reported the ordinary way, as a new version of
round.settings. - An override MUST exist, and MUST be loud. A lock with no escape hatch gets defeated locally or disabled globally. An override is a new version of
round.settingswith the actor inchanged_by, like any other change.
What this is worth, stated honestly. A declared lock is a self-assertion like everything else a simulator reports, and it proves nothing on its own. What it changes is the shape of the failure: from somebody nudged the wind on the 14th to somebody deliberately defeated a lock that was declared active and recorded in the journal. That is a narrower attack, a much clearer record, and a far easier thing for a committee to rule on.
2.2 Deadlines, and why the bay clock cannot enforce one#
An event may set round_deadline — a UTC instant after which no stroke counts, regardless of when a player teed off. It is what lets a winner be declared at a fixed moment even though some entrants started too late to finish: their rounds close where they stand and score as partials.
The bay's system clock cannot enforce it. The clock is settable by whoever is standing at the machine, and a player who is out of time and behind has an obvious incentive. A deadline checked against the local clock is not a deadline.
So the engine maintains a time anchor, recorded in the session manifest:
| source | trusted | how |
|---|---|---|
signed_remote | yes | signed timestamp from SGA, current |
signed_cached | yes | last signed timestamp, advanced by the engine's monotonic clock |
roughtime | yes | an independent signed-time protocol |
system_clock | no | the bay's own clock |
none | no | nothing to go on |
signed_cached is the offline case and it is the interesting one. A bay with no internet still holds a signed timestamp from its last contact, and advances it with a clock that cannot be wound back. Setting the system clock does not move it. Attempting to move the monotonic clock backwards is itself detectable and is recorded.
Rules:
- An event declares
min_time_anchor. Defaultsigned_cached. - If the engine cannot reach that standard, the round is recorded and marked
unverifiedfor deadline purposes. It is not refused, and it is not silently accepted either — the same posture as everywhere else here. - The anchor in force is written into the session manifest, so months later it is answerable whether a deadline was enforced against trusted time or against a clock someone could have set.
A warning precedes the cutoff. The engine emits notice.round_ending with the time remaining at a sensible interval before the deadline, then request.end_round when it passes. For a competition this is the difference between a player choosing to hurry and a player losing a hole they were standing in — and a deadline nobody was warned about will be appealed, correctly.
deadline_grace_ms decides whether the hole in progress may be finished. Zero for a hard close.
3. Input integrity#
swing_input: launch_monitor_only means every counted stroke traces to a physical measurement from an attested device.
- Every stroke MUST correlate to a launch monitor shot event within the configured window. A stroke the simulator counted with no corresponding measurement is reported as
unmeasured_stroke. - Mouse, keyboard, gamepad and touch swing input MUST be disabled by the simulator, which then declares only
launch_monitorinround.settings.swing_inputs. - Simulated or replayed shot injection MUST be rejected. In practice this is the attestation chain doing the work: an injected shot has no valid device signature over the session nonce, so it cannot be forged without the device key (02).
unmeasured_stroke is deliberately not fatal. Gimmes, penalty drops and concessions are legitimately unmeasured, and the rules package says which are permitted. The event is recorded either way and the ruleset decides.
4. Identity assurance for competition#
Our three levels, with the mechanisms attached.
| level | mechanism | for |
|---|---|---|
basic | account, email or phone | recreational, social, unranked |
verified | account + QR check-in + proximity token (07) | ranked play, leagues, most events |
championship | verified + live capture at the bay, reviewable after the fact | prize events, national championships |
4.1 Championship-tier capture — deferred, not scheduled#
Status: parked as of September 2026. An advanced feature, not in the alpha and not in the first public release. Specified here so the data model has a place for it and nobody designs it out — but nothing below should be built yet, and the compliance work in §4.2 must be settled first.
For events that require it: a webcam at the bay captures stills at check-in and swing video during play, retained for review of prize-winning rounds by a human, optionally triaged by a model.
Machines flag. Humans adjudicate. Our rule, and it applies with full force here — an automated identity mismatch is a reason to look, never a finding.
4.2 Where I would not go, and why#
Registration capturing a government ID photo, deriving a face template from it, and matching that template against a bay webcam is technically straightforward and legally the most hazardous thing proposed for this platform.
A face template is a biometric identifier, and biometric identifiers are separately regulated from ordinary personal data.
- Texas CUBI (Bus. & Com. Code §503.001) — SGA is a Texas company, so this is the home statute. Capturing a biometric identifier for a commercial purpose requires informed consent before capture, bars sale, and mandates destruction within a set period. Enforcement is by the Attorney General at up to $25,000 per violation, and Texas has pursued it aggressively against large technology companies in recent years.
- Illinois BIPA — the one that produces catastrophic exposure, because it carries a private right of action with statutory damages per violation. A national membership platform will have Illinois members. Class actions under BIPA have produced nine- and ten-figure settlements.
- Washington, Colorado, and a growing set of state privacy statutes treat biometrics as sensitive data requiring opt-in consent.
- GDPR Article 9 makes biometric data for identification a special category, prohibited absent an explicit basis — relevant the moment SGA has a European member or a European event.
Retaining driver's license and passport images compounds it: that is identity-document data with its own breach-notification regime, and it is a uniquely attractive target.
The architecture that gets the capability without the liability is the one the industry already converged on: use a specialized identity-verification provider. Persona, Onfido, Jumio, ID.me and others operate in this space, and they perform the document check and the face match.
They carry the compliance program, the retention schedule, the consent flows and the audit. We do not.
SGA stores the assertion, not the biometric:
{
"assurance": "championship",
"verified_by": "vendor-x",
"verified_at": "2026-09-14T18:02:11Z",
"document_type": "drivers_license",
"match_reference": "opaque-token",
"expires_at": "2027-09-14T00:00:00Z"
}No image, no template, no document number in SGA's database. Re-verification at the bay calls the provider with the same opaque reference. If SGA is breached, no biometric leaves the building, because none was ever in it.
Swing video is a much lighter matter and is worth keeping distinct: it is video of a person performing an activity, not a biometric template, provided we do not run face recognition over it. The moment we do, it becomes biometric collection and inherits everything above. Keep those two purposes separated in the code and in the consent language, exactly as diagnostic frames and the training corpus are kept separate.
4.3 Rules#
- Biometric identification MUST be opt-in, per event, with consent captured before collection and recorded.
- SGA MUST NOT store raw biometric templates or identity-document images in the platform database while a provider-backed alternative exists.
- Swing video retention MUST have a stated purpose, a stated retention period, and MUST NOT be repurposed for identification or for model training without separate, explicit, purpose-specific consent.
- Championship capture MUST be disclosed at registration, not at the bay. Nobody should meet the camera for the first time when they arrive to play.
5. What a simulator vendor has to build#
The short version, for the partnership conversation. To host sanctioned SGA events, a simulator needs:
- Accept an Event Rules Package from the SGA Engine, which has already verified SGA's signature on it (§1.1). Verifying it again is optional.
- Publish
round.settingsin every round and on every change — the same declaration that makes a casual round count fully toward a player rating. For an event it is the return leg of the round trip in §2. - Answer
request.apply_ruleswith accepted or refused. - Disable non-measured swing input on request and report it disabled.
- Expose or permit hashing of the course file.
- Emit shot records with
originandoutcome(06).
That is a genuinely small list, and none of it requires exposing anything a vendor considers proprietary. Points 1–4 are the ones that generally have to be built, and they are also the ones that let a vendor advertise their platform as tournament-capable — which is the reason for them to do it.