Skip to content

Latest commit

 

History

110 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fastform

tests submit time dependencies python platform license

A browser spends 2–5s rendering a form to submit it. The form only needs one HTTP request. fastform sends that request — about 300ms, start to recorded — and reads the reply to prove the row actually landed.

Submits a Google Form by POSTing directly to formResponse — no browser, no page render, no automation driver.

Stdlib only, Python 3.11+ (developed on 3.14). Live cookie reads on macOS/Linux; stored (paste-once) auth works on any OS, including Windows.

  • Design: docs/superpowers/specs/2026-08-05-fastform-design.md
  • Plan: docs/superpowers/plans/2026-08-05-fastform-phases-0-4.md

How it works

A browser spends seconds rendering the form, running its scripts, and posting the result. fastform skips all of it: it fetches the form once, scrapes the few hidden fields the POST needs, and sends a single formResponse request over one warm connection — then reads the reply to decide what actually happened.

The one thing it will not do is fake your session. Google rotates the Chrome session token every few minutes, so a copied cookie header is dead within minutes. On macOS/Linux (the default), fastform instead reads Chrome's own cookie store live, at fire time — that is why the setup below is about Chrome, not about pasting a token. A pasted-header ("stored") mode also exists for Windows or portable/CI use; see Authentication for how the two modes differ and their tradeoffs.

Setup

  1. Sign into Google in Chrome. fastform reads your cookies straight from Chrome when it runs — there is no token to copy and nothing to keep fresh.

  2. Choose which Chrome profile submits. A machine can have several profiles on different accounts, and the tool refuses to guess — the wrong profile submits as the wrong person. List what you have:

    python3 -c "from fastform import cookies; [print(f'{p.name}: {e}') for p,e in cookies.resolve_accounts()]"

    Then create your local config from the template and pin one:

    cp config/profile.toml.example config/profile.toml
    # config/profile.toml
    [chrome]
    profile = "Profile 1"

    config/profile.toml is gitignored, so your choice stays on your machine — profile names differ per machine and must not be shared. (Or pass --profile "Profile 1" on the command line to override it, and skip the file.)

  3. Generate your answers file from the form. Instead of hand-writing match strings, let scaffold read the form's real questions:

    # blank stubs — fill in the value = "" lines:
    python3 tools/scaffold.py -u <viewform-url>
    # or answer each question interactively:
    python3 tools/scaffold.py -u <viewform-url> -i

    It writes config/answers.toml with each question's exact text as the match, choice options listed, and unfillable questions (file upload, grid) commented out. It only reads the form — it submits nothing. It refuses to overwrite an existing answers file without --force. You can still hand-edit the result; each entry matches a question by text and supplies a value.

On first run macOS may prompt to let the tool read "Chrome Safe Storage" from the Keychain. Approve it — and do so well before you need to move fast, so the prompt is not what slows you down at go-time.

Authentication

fastform gets your session cookie one of two ways, chosen by [auth] method in config/profile.toml (unset falls back to the platform default):

OS live (default: macOS/Linux) stored (default: Windows)
macOS reads Chrome's cookie store live at fire time — nothing to do beyond picking a profile opt-in; set method = "stored"
Linux same as macOS opt-in; set method = "stored"
Windows not available (Chrome's app-bound encryption has no stdlib/openssl decrypt path) the only option — no profile.toml edit needed

live (macOS/Linux, the default): nothing to set up beyond the Chrome profile above — every run reads a fresh cookie straight from Chrome.

stored (Windows, or portable/CI use on any OS): paste a cookie header once, then verify it:

cp config/cookies.txt.example config/cookies.txt
# paste your Cookie: header into config/cookies.txt, then:
python3 tools/auth-check.py

auth-check confirms the pasted header authenticates and prints which account it resolves to. There is no auto-refresh, and none is planned — a spike against Google's RotateCookies endpoint returned only the *SIDCC cookies, never the __Secure-1PSIDTS token that actually expires. The stored header lasts ~20 minutes before Google rotates it stale, so re-paste it before each session. config/cookies.txt is gitignored and is full account access, not just Forms — never commit it or share it.

Use

Pass either the full https://docs.google.com/forms/d/e/.../viewform URL or a shortened https://forms.gle/… link — short links are resolved automatically, and the resolved target is printed before anything counts.

The whole flow, start to finish:

python3 tools/scaffold.py -u <url>              # 1. write answers.toml from the form
python3 tools/dryrun.py   -u <url>              # 2. confirm the request — sends nothing
python3 tools/probe.py    -u <url>              # 3. (before a race) check throttling
python3 -m fastform.race  -u <url> -t 17:59:30  # 4a. fire at a scheduled time
python3 -m fastform.fire  -u <url>              # 4b. or fire once, right now

Each step is detailed below.

Dry run first — build the request and print it, sending nothing:

python3 tools/dryrun.py --url <viewform-url>

Use this to confirm your answers match the form's questions before it counts. It exercises the whole pipeline except the send.

Fire — fill and submit, exactly once:

python3 -m fastform.fire --url <viewform-url>

fire submits once and never retries (see Safety). It prints the outcome, the HTTP status, the elapsed time, and which account it submitted as, then exits with one of these codes:

Code Meaning
0 Recorded (or already responded)
1 Rejected — payload wrong
2 Not parseable — closed, deleted, or a redirect
3 Form is closed
4 Auth failed — that Chrome profile is not signed in
5 Undetermined — check the response sheet
6 Gaps or blockers — nothing sent
7 Config — no profile set, or cookies unreadable

(All commands also accept the short flags -u/--url and -p/--profile.)

Race — fire at a scheduled time, polling until it lands:

For a contest where the form opens at an unknown instant, race pre-warms the connection ahead of time, then from the target moment polls until one submission is accepted, stopping the instant it records:

python3 -m fastform.race -u <viewform-url> -t 17:59:30

Because Google Forms has no server-side dedupe, an ambiguous timeout (UNKNOWN) cannot be resolved, so you choose the policy: --on-unknown retry (default) keeps trying, --on-unknown stop bails; --max-attempts (default 2) caps the total sends, so a duplicate is at most one extra row.

Flag Default Meaning
-t, --at (required) target time today, HH:MM:SS
-l, --lead 30 seconds before the target to pre-warm
-i, --interval 0.25 seconds between attempts
-d, --deadline 60 seconds after the target to keep trying
-s, --send-lead 0.0 seconds before the target to begin firing
-k, --on-unknown retry retry or stop
-n, --max-attempts 2 cap on total sends (= max rows)

Same exit codes as fire. Timing is only as accurate as your Mac's clock — sync via NTP before a real race.

Calibrate — measure your real fire latency (warm vs cold):

Before a race, find out whether you are fast enough. calibrate submits N POSTs on a warm connection and N on cold sockets, then reports p50/p95/max and what warming buys:

python3 tools/calibrate.py -u <throwaway-url> -n 50 --yes
warm (n=50):  p50 312ms · p95 480ms · max 890ms · recorded 50/50
cold (n=50):  p50 640ms · p95 920ms · max 1400ms · recorded 50/50
warm-up saves ~328ms at p50 · suggested timeout ~1.4s (cold p95 ×1.5)

Every POST appends a row, so a run submits 2×N real rows. It therefore requires --yes and refuses otherwise, printing the row count first — point it only at a throwaway form you will clear.

Probe — check whether Google throttles the race poll (GET-only, no rows):

race polls with a GET every 0.25s. Before a real race, confirm that rate is not throttled. probe fires GETs at the race interval on one warm connection and reports status codes, latency, and any throttle signals. It never POSTs, so it appends no rows and is safe on any form — no --yes needed:

python3 tools/probe.py -u <viewform-url> -n 0.25 -d 60
status    200 x240
latency   p50 631ms · p95 812ms · max 1290ms
throttle  0 hard · 0 latency-spike
verdict   SAFE at 0.25s over 60s — race interval holds

A THROTTLED verdict (HTTP 429/403 or a google.com/sorry interstitial) means you should raise race --interval.

Tests

python3 -m unittest discover -s tests -v

No network: the transport tests run against a local server, and everything else runs against committed fixtures.

Safety

fastform is built around one hard fact: Google Forms has no server-side duplicate protection. A "limit to 1 response" setting does not bind a direct POST, and there is no already-responded state — every accepted POST appends a row. Three invariants follow from that:

  • fire sends once; race sends at most --max-attempts times. The client's RECORDED result is the only thing preventing a duplicate row, so fire never resends, and race stops the instant a send records — and on an ambiguous UNKNOWN too, unless you opt into --on-unknown retry.
  • The Chrome profile is required. Unset, fire lists your accounts and exits without sending, rather than guess which account should submit.
  • Uncertain means uncertain. A network error mid-submit reports code 5 (undetermined) and tells you to check the response sheet — it never resends, because a false retry is the worst outcome.

Status

Built and tested end to end against a real form:

  • Pipeline + fire (Phases 0–4): cookies, parse, match, compile, classify, transport, and the single-shot submit CLI.
  • dryrun — preview the request without sending.
  • race — scheduled, pre-warmed, bounded-retry firing (Phase 6 discipline).
  • calibrate — warm-vs-cold latency profiling (Phase 5, experiment E7).
  • probe — GET-only rate-limit check for the race interval (Phase 5, E8).
  • forms.gle links — resolved automatically across every entrypoint that takes a URL.
  • scaffold — generate answers.toml from the form (stub or interactive).

Not yet built: connection survival over hours (E9), the last of Phase 5's long-running experiments. It stays open because a long race --lead makes it answerable with shipped code.

Deliberately out of scope

The Phase 7 daemon — clipboard/LAN intake, a persistent ARMED_HOLDING state, and a TUI — will not be built, and E6 (token/fbzx staleness when arming early) closes with it, since it existed only to de-risk holding a compiled payload for hours.

race already covers the scheduled fire, connection pre-warm, open-polling and bounded retry the daemon was going to provide, and dryrun surfaces unmatched required answers hours ahead in one command. What remains is a few seconds of URL-intake latency — not worth a long-running process, a 5 ms clipboard poll, and an HTTP listener on your LAN that can arm a tool which submits as you.

If intake latency ever proves to be the real bottleneck at a live event, add a --watch-clipboard flag to race (~20 lines, replacing -u) rather than reopening the daemon. Full reasoning: docs/superpowers/specs/2026-08-11-daemon-scope-closure-design.md.

License

MIT — see LICENSE.

About

Submit a Google Form in ~300ms by POSTing straight to formResponse instead of driving a browser. Cross-OS auth: live Chrome-cookie reads on macOS/Linux, or a pasted header on any OS incl. Windows. Fires exactly once, and can race a form open at a scheduled time. Python 3.11+, stdlib only.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages