Friday Grocery Cart turns a normal shared Notion checklist into a weekly Kroger cart without converting the document into a database.
Every Friday at 9:00 p.m. Eastern, a Val Town interval reads the Notion page, finds unchecked non-empty to-do blocks under the Kroger heading, resolves their text to pickup-eligible products at a configured store, adds confirmed matches to the authenticated customer's Kroger cart, and checks off each Notion block only after Kroger reports success. The customer reviews the cart and completes checkout in Kroger manually.
The Notion API exposes blocks, text, headings, checkbox state, and block IDs. The application still owns the business rules that give those blocks meaning.
The first end-to-end integration test passed on August 29, 2026:
Lettuce.0001111018167.Lettuce.A production mapping run completed on September 3, 2026:
Configured production context:
Grocery List303c869b2f04804bbf24f393fd074bd102900540Notion shared page ↓ read blocks Val Town interval and business rules ↓ search pickup products Kroger Products API ↓ add remembered/confirmed matches Kroger Cart API ↓ successful response Notion Update Block API ↓ Customer reviews cart and checks out manually
Files:
main.ts — protected admin page, OAuth routes, diagnostics, and manual-run endpointlib.ts — Notion parser, Kroger clients, matching, OAuth refresh, cart writes, and state transitionscron.ts — scheduled Friday entrypointdry-run.ts — read-only parser diagnosticREADME.md — setup and operational documentationPersistent state uses Val-scoped std/blob storage:
kroger:oauth — Kroger access and refresh tokensoauth:state:* — short-lived OAuth anti-forgery statematch:<normalized text> — remembered checklist-text-to-UPC mappingsrun:<date>:<notion block id> — per-item execution ledgerrun:latest — latest run reportCredentials belong in Val Town environment variables, never blob storage or source code.
The page remains a normal shared document. No Notion database is required.
Example:
## 09/04/26 - next shop
### Kroger
- [ ] Lettuce
- [ ] 2x whole milk
- [x] Coffee
### Costco
- [ ] Paper towels
Current parsing rules:
to_do blocks under a heading exactly equal to Kroger, ignoring case.2x, 2×, or 2 as quantity; otherwise use quantity 1.Open the Val's Environment Variables page and create the following values:
| Variable | Secret? | Purpose |
|---|---|---|
NOTION_TOKEN | Yes | Notion internal connection token with access to the grocery page |
KROGER_CLIENT_ID | Yes | Client ID from the Kroger Developer Portal |
KROGER_CLIENT_SECRET | Yes | Client secret from the Kroger Developer Portal |
KROGER_REDIRECT_URI | No | Exact deployed OAuth callback URL |
KROGER_LOCATION_ID | No | Kroger location used for product and pickup filtering |
SETUP_KEY | Yes | Protects admin, diagnostics, connect, and manual-run routes |
LIVE_CART | No | Master write switch; must equal the string true for cart and Notion mutations |
Prefilled environment-variable pages:
👉 Add NOTION_TOKEN here: https://www.val.town/x/pchinjr/friday-grocery-cart/environment-variables?key=NOTION_TOKEN
👉 Add KROGER_CLIENT_ID here: https://www.val.town/x/pchinjr/friday-grocery-cart/environment-variables?key=KROGER_CLIENT_ID
👉 Add KROGER_CLIENT_SECRET here: https://www.val.town/x/pchinjr/friday-grocery-cart/environment-variables?key=KROGER_CLIENT_SECRET
👉 Add KROGER_REDIRECT_URI here: https://www.val.town/x/pchinjr/friday-grocery-cart/environment-variables?key=KROGER_REDIRECT_URI
👉 Add KROGER_LOCATION_ID here: https://www.val.town/x/pchinjr/friday-grocery-cart/environment-variables?key=KROGER_LOCATION_ID
👉 Add SETUP_KEY here: https://www.val.town/x/pchinjr/friday-grocery-cart/environment-variables?key=SETUP_KEY
👉 Add LIVE_CART here: https://www.val.town/x/pchinjr/friday-grocery-cart/environment-variables?key=LIVE_CART
For this deployment:
KROGER_REDIRECT_URI=https://pchinjr--373b716ca36411f1a6f51607ee4eb77e.web.val.run/auth/kroger/callback KROGER_LOCATION_ID=02900540 LIVE_CART=false
Generate SETUP_KEY as a long random value. Do not reuse a password. The HTTP Val must remain public so Kroger can reach its OAuth callback, so every administrative route is separately protected by this key.
Keep LIVE_CART=false during setup and every dry run. Turn it on only after Notion parsing, store selection, OAuth, and product matching have been verified.
You must be able to create an internal connection in the target Notion workspace.
PrefilledGroceryCart.NOTION_TOKEN environment variable.••• menu.The token alone is insufficient. If the page was not shared with the connection, Notion returns 404 object_not_found even when the page ID is correct.
Only the Grocery List page needs to be connected. It does not need to be published, converted into a database, or exposed to the web.
KROGER_CLIENT_ID.KROGER_CLIENT_SECRET.KROGER_REDIRECT_URI.The application uses two OAuth grant types:
product.compact for stores and product search.cart.basic:write for acting on behalf of the customer's cart.The Val never receives or stores the customer's Kroger password or payment information.
After setting the Kroger credentials and callback:
/admin?key=<SETUP_KEY>state record./auth/kroger/callback with a temporary code.state.The requested scope can add products to the cart. It does not grant checkout or payment permission.
Use the protected endpoint below to look up stores:
GET /api/stores?zip=<ZIP>&key=<SETUP_KEY>
Copy the desired locationId into KROGER_LOCATION_ID.
Product searches include:
filter.locationId=<KROGER_LOCATION_ID> filter.fulfillment=csp
The csp filter restricts candidates to products Kroger reports as eligible for curbside pickup at that location.
Important limitation: the public Cart API adds items but does not choose a fulfillment mode, reserve a pickup window, or place an order. The customer confirms pickup, selects a time, reviews substitutions and prices, and checks out in Kroger.
A broad human description can return several valid products. For example, Lettuce returned iceberg, romaine, green leaf, shredded, hearts, and organic products.
Current policy:
match:<normalized text>.review.Remembered mappings do not contain price conditions. The selected product is added whenever Kroger reports that exact UPC as pickup-eligible. This deliberately prioritizes a correct cart over a full cart.
For every eligible Notion block:
unchecked → matched → attempting cart write → Kroger success → ledger marked added → Notion checked
The Notion checkbox is the commit step from the user's perspective.
Before a live cart request, the Val writes an attempting ledger record. After Kroger returns success, it writes added, then checks the Notion block.
If a network error occurs after the request begins, the ledger becomes uncertain. Automatic retry is blocked because Kroger's public cart surface cannot reliably read or remove items. A human must inspect the cart before resolving an uncertain item.
Keep LIVE_CART=false, then call:
GET /api/dry-run?key=<SETUP_KEY>
Expected output contains only unchecked, non-empty Kroger items and mutationAttempted: false.
GET /api/products?q=Lettuce&key=<SETUP_KEY>
Review UPC, description, size, price, and curbside fulfillment.
POST /api/run?live=false&key=<SETUP_KEY>
This resolves remembered/exact candidates but does not add to Kroger or update Notion.
LIVE_CART=true.POST /api/run?live=true&key=<SETUP_KEY>.added and none in failed.LIVE_CART=false while reviewing results.cron.ts calls runShoppingList({ live: true }), but actual writes still require LIVE_CART=true.
The interval uses:
0 * * * *
Val Town cron expressions use UTC and do not adjust for daylight-saving time. The interval therefore wakes at the top of each hour, and cron.ts uses Intl.DateTimeFormat with America/New_York to continue only when the local clock is Friday at 21:00. Every other hourly invocation exits without reading the grocery list or calling Kroger. This keeps execution at exactly 9:00 p.m. Eastern during both EDT and EST.
Use the protected admin page to inspect configuration state and the latest run report. Never publish a run report on an unprotected route because it may reveal private grocery-list contents.
uncertain ledger entries.LIVE_CART only when the review and notification workflow is ready.SETUP_KEY as a password because the HTTP endpoint must be publicly reachable for OAuth.state validation enabled.product.compact and cart.basic:write.uncertain outcomes manually rather than retrying blindly.The infrastructure and first real integration are working. The remaining work is primarily product judgment and operational UX, not connectivity. Notion is acting as the collaborative human interface; Val Town supplies the hidden schema, parser, state machine, matching memory, OAuth lifecycle, idempotency rules, and side effects; Kroger remains the final review and checkout surface.