Repository files navigation

fastform

testssubmit timedependenciespythonplatformlicense

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):

OSlive (default: macOS/Linux)stored (default: Windows)
macOSreads Chrome's cookie store live at fire time — nothing to do beyond picking a profileopt-in; set method = "stored"
Linuxsame as macOSopt-in; set method = "stored"
Windowsnot 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:

CodeMeaning
0Recorded (or already responded)
1Rejected — payload wrong
2Not parseable — closed, deleted, or a redirect
3Form is closed
4Auth failed — that Chrome profile is not signed in
5Undetermined — check the response sheet
6Gaps or blockers — nothing sent
7Config — 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.

FlagDefaultMeaning
-t, --at(required)target time today, HH:MM:SS
-l, --lead30seconds before the target to pre-warm
-i, --interval0.25seconds between attempts
-d, --deadline60seconds after the target to keep trying
-s, --send-lead0.0seconds before the target to begin firing
-k, --on-unknownretryretry or stop
-n, --max-attempts2cap 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

fastform

testssubmit timedependenciespythonplatformlicense

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):

OSlive (default: macOS/Linux)stored (default: Windows)
macOSreads Chrome's cookie store live at fire time — nothing to do beyond picking a profileopt-in; set method = "stored"
Linuxsame as macOSopt-in; set method = "stored"
Windowsnot 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:

CodeMeaning
0Recorded (or already responded)
1Rejected — payload wrong
2Not parseable — closed, deleted, or a redirect
3Form is closed
4Auth failed — that Chrome profile is not signed in
5Undetermined — check the response sheet
6Gaps or blockers — nothing sent
7Config — 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.

FlagDefaultMeaning
-t, --at(required)target time today, HH:MM:SS
-l, --lead30seconds before the target to pre-warm
-i, --interval0.25seconds between attempts
-d, --deadline60seconds after the target to keep trying
-s, --send-lead0.0seconds before the target to begin firing
-k, --on-unknownretryretry or stop
-n, --max-attempts2cap 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

fastform

testssubmit timedependenciespythonplatformlicense

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):

OSlive (default: macOS/Linux)stored (default: Windows)
macOSreads Chrome's cookie store live at fire time — nothing to do beyond picking a profileopt-in; set method = "stored"
Linuxsame as macOSopt-in; set method = "stored"
Windowsnot 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:

CodeMeaning
0Recorded (or already responded)
1Rejected — payload wrong
2Not parseable — closed, deleted, or a redirect
3Form is closed
4Auth failed — that Chrome profile is not signed in
5Undetermined — check the response sheet
6Gaps or blockers — nothing sent
7Config — 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.

FlagDefaultMeaning
-t, --at(required)target time today, HH:MM:SS
-l, --lead30seconds before the target to pre-warm
-i, --interval0.25seconds between attempts
-d, --deadline60seconds after the target to keep trying
-s, --send-lead0.0seconds before the target to begin firing
-k, --on-unknownretryretry or stop
-n, --max-attempts2cap 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

fastform

testssubmit timedependenciespythonplatformlicense

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):

OSlive (default: macOS/Linux)stored (default: Windows)
macOSreads Chrome's cookie store live at fire time — nothing to do beyond picking a profileopt-in; set method = "stored"
Linuxsame as macOSopt-in; set method = "stored"
Windowsnot 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:

CodeMeaning
0Recorded (or already responded)
1Rejected — payload wrong
2Not parseable — closed, deleted, or a redirect
3Form is closed
4Auth failed — that Chrome profile is not signed in
5Undetermined — check the response sheet
6Gaps or blockers — nothing sent
7Config — 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.

FlagDefaultMeaning
-t, --at(required)target time today, HH:MM:SS
-l, --lead30seconds before the target to pre-warm
-i, --interval0.25seconds between attempts
-d, --deadline60seconds after the target to keep trying
-s, --send-lead0.0seconds before the target to begin firing
-k, --on-unknownretryretry or stop
-n, --max-attempts2cap 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

fastform

testssubmit timedependenciespythonplatformlicense

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):

OSlive (default: macOS/Linux)stored (default: Windows)
macOSreads Chrome's cookie store live at fire time — nothing to do beyond picking a profileopt-in; set method = "stored"
Linuxsame as macOSopt-in; set method = "stored"
Windowsnot 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:

CodeMeaning
0Recorded (or already responded)
1Rejected — payload wrong
2Not parseable — closed, deleted, or a redirect
3Form is closed
4Auth failed — that Chrome profile is not signed in
5Undetermined — check the response sheet
6Gaps or blockers — nothing sent
7Config — 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.

FlagDefaultMeaning
-t, --at(required)target time today, HH:MM:SS
-l, --lead30seconds before the target to pre-warm
-i, --interval0.25seconds between attempts
-d, --deadline60seconds after the target to keep trying
-s, --send-lead0.0seconds before the target to begin firing
-k, --on-unknownretryretry or stop
-n, --max-attempts2cap 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

fastform

testssubmit timedependenciespythonplatformlicense

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):

OSlive (default: macOS/Linux)stored (default: Windows)
macOSreads Chrome's cookie store live at fire time — nothing to do beyond picking a profileopt-in; set method = "stored"
Linuxsame as macOSopt-in; set method = "stored"
Windowsnot 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:

CodeMeaning
0Recorded (or already responded)
1Rejected — payload wrong
2Not parseable — closed, deleted, or a redirect
3Form is closed
4Auth failed — that Chrome profile is not signed in
5Undetermined — check the response sheet
6Gaps or blockers — nothing sent
7Config — 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.

FlagDefaultMeaning
-t, --at(required)target time today, HH:MM:SS
-l, --lead30seconds before the target to pre-warm
-i, --interval0.25seconds between attempts
-d, --deadline60seconds after the target to keep trying
-s, --send-lead0.0seconds before the target to begin firing
-k, --on-unknownretryretry or stop
-n, --max-attempts2cap 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

fastform

testssubmit timedependenciespythonplatformlicense

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):

OSlive (default: macOS/Linux)stored (default: Windows)
macOSreads Chrome's cookie store live at fire time — nothing to do beyond picking a profileopt-in; set method = "stored"
Linuxsame as macOSopt-in; set method = "stored"
Windowsnot 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:

CodeMeaning
0Recorded (or already responded)
1Rejected — payload wrong
2Not parseable — closed, deleted, or a redirect
3Form is closed
4Auth failed — that Chrome profile is not signed in
5Undetermined — check the response sheet
6Gaps or blockers — nothing sent
7Config — 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.

FlagDefaultMeaning
-t, --at(required)target time today, HH:MM:SS
-l, --lead30seconds before the target to pre-warm
-i, --interval0.25seconds between attempts
-d, --deadline60seconds after the target to keep trying
-s, --send-lead0.0seconds before the target to begin firing
-k, --on-unknownretryretry or stop
-n, --max-attempts2cap 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

fastform

testssubmit timedependenciespythonplatformlicense

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):

OSlive (default: macOS/Linux)stored (default: Windows)
macOSreads Chrome's cookie store live at fire time — nothing to do beyond picking a profileopt-in; set method = "stored"
Linuxsame as macOSopt-in; set method = "stored"
Windowsnot 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:

CodeMeaning
0Recorded (or already responded)
1Rejected — payload wrong
2Not parseable — closed, deleted, or a redirect
3Form is closed
4Auth failed — that Chrome profile is not signed in
5Undetermined — check the response sheet
6Gaps or blockers — nothing sent
7Config — 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.

FlagDefaultMeaning
-t, --at(required)target time today, HH:MM:SS
-l, --lead30seconds before the target to pre-warm
-i, --interval0.25seconds between attempts
-d, --deadline60seconds after the target to keep trying
-s, --send-lead0.0seconds before the target to begin firing
-k, --on-unknownretryretry or stop
-n, --max-attempts2cap 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