Skip to content

[Feature] Pay for shop orders with an OpenBanking card; cross-app purchase tasks - #86

Open
aaronsmulktis wants to merge 29 commits into
mainfrom
aaronsmulktis/shop-with-bank
Open

aaronsmulktis wants to merge 29 commits into
mainfrom
aaronsmulktis/shop-with-bank

Conversation

@aaronsmulktis

@aaronsmulktis aaronsmulktis commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Lets the online shop take payment with a credit card held in the OpenBanking app, and adds tasks that are scored on both apps at once.

Until now every OpenBanking task was read-then-act. The bank was read-only, so the agent looked a figure up and the scored change landed in todo, calendar or messenger. With this PR, checkout charges a card through a single write path into the bank. A purchase is then a change in two apps' state: the shop gains an order, and the bank gains a pending charge that lowers the card's available credit by exactly the order total.

The cross-app step is real, not a form fill. The card number, expiry and CVV appear only on the card's page in OpenBanking, each masked until clicked, and checkout refuses an order without all three.

uv run launch_agent.py agent=GPT-5.5-computer-use tasks=openbanking \
    task_name=buy_the_console_table_with_the_business_card \
    browsergym_env_args.max_steps=40

What's in it

OpenBanking: cards can be charged (openbanking_app/main.py, config/apps/openbanking/)

  • Cards gain a CVV, masked on the card's page until revealed. The label is in all three content packs (default, german, mandarin).
  • authorize_card_purchase is the bank's only write path. It matches the card number, expiry, CVV and cardholder name against a seeded card, and refuses a card whose expiry has passed. It then checks the amount against available_credit plus overlimit_grace (default 10.00). There are three outcomes:
    • Approve: posts a pending Card row and lowers available credit.
    • Approve in the grace band: also posts a zero-amount OVERLIMIT NOTICE row naming the overage.
    • Decline for insufficient credit: posts one zero-amount DECLINED row naming the merchant and the attempted amount (DECLINED - OPENAPPS SHOP $21,499.95 - insufficient available credit). No money moves and the balances stay the same. Real card activity feeds show declined authorizations the same way. Without the row, a refused checkout left both apps exactly as they were before the attempt.
    • Decline on card details (wrong number, CVV, name or expiry): writes nothing, so an agent can fix a typo and resubmit without failing a task.
  • Pending rows get a negative position, so they sort above the seeded ledger without renumbering it.
  • Decline messages are vague on purpose. Naming the field that failed would let an agent brute-force the CVV. The declined row names the attempted amount, not the shortfall.
  • Expiry is parsed as a real month/year (09/29, 9/2029, 0929, …), so 13/29 is rejected. Expiry is judged against as_of_date (default 2026-08-26, the seeded statement cycle), not the wall clock, so the seeded 09/29 card can't expire under a task and turn every purchase into a decline. Set as_of_date: null to use the real date.

Online shop: checkout uses the bank (onlineshop_app/main.py, config/apps/onlineshop/default.yaml)

  • enable_credit_card_check now defaults to true. Checkout asks for card number, expiry, CVV and cardholder name, and authorizes against the bank.
  • Checkout rejects a malformed or past expiry before asking the bank ("Enter the card's expiry date as MM/YY." / "That expiry date has passed."), the way a real payment form does. Nothing is written.
  • allowed_credit_cards is matched against the stored card's card_brand, not anything the shopper types. Ledger rows are labelled OPENAPPS SHOP {order_id} (card_descriptor), so each charge traces back to its order.
  • Orders record card_last4. The order card shows a "Paid with" row.

Tasks and rewards (tasks/tasks.py, config/tasks/openbanking.yaml, docs/tasks.md)

  • BuyWithCardTask builds the target state: an order carrying the card's last four, plus the matching bank charge, plus the overlimit row when the total falls in the grace band.

  • DeclinedCardPurchaseTask handles a checkout the app refuses. It scores two things:

    • the cart line, which stays because the purchase failed. This fails an agent that never opened a browser.
    • the bank's DECLINED row. This fails an agent that filled the cart and never submitted checkout, which used to leave identical state, while the shortfall the todo asks for could be worked out from the cart total and the card page.

    A second refused attempt posts a second row and fails, like a duplicate message would.

  • AppStateComparison gains normalize_purchases. It ignores the random order id, the wall-clock date, the order id embedded in the ledger row, and ledger id/position. It's opt-in, so every other task still fails on a stray purchase. Both purchase task classes use it, and CompositeTask turns it on when a subtask checks out.

  • Both task classes refuse to build a target for a total the app would treat differently, for example a purchase priced into a decline. Mispricing raises an error instead of scoring something the app can't produce.

  • Four new tasks in tasks=openbanking:

    task case
    buy_the_console_table_with_the_business_card approved purchase
    buy_the_console_table_and_log_the_charge purchase, then a todo naming the credit left after the charge
    buy_the_mirrors_just_over_the_card_limit approved in the $10 grace band, posting two ledger rows
    attempt_the_copier_far_over_the_card_limit declined, posting a DECLINED row; the agent logs the shortfall as a todo

Eval infra

  • vLLM_agent.py fails at startup when agent.hostname is missing. Previously it built http://None:8000 and surfaced a bare RetryError three minutes later.
  • launcher.py writes errored, err_msg and a truncated stack_trace to the W&B run summary. A crashed episode no longer looks like an agent that ran and scored 0.
  • New scripts/serve_vllm.sbatch keeps a vLLM server running for conduct_slurm.sh to find. Account, QOS and partition are placeholders.

The branch was merged with current main. Conflicts were in the shop checkout (it keeps main's new layout, with the card fields inside it), launcher.py, theme.py and docs/inspecting_databases.md.

Reviewer notes

  • Default change: a plain uv run launch.py shop now asks for card details at checkout. No existing task checks out (the only shop task is navigate_to_onlineshop), but apps.onlineshop.enable_credit_card_check=false restores the old behaviour for anyone who relied on it.
  • max_steps: the default is 10, which is too low for these tasks. Use about 40.
  • Fixed prices: unit_price in each task has to match the catalog being served (content=webshop by default), because the reward can't look prices up. The task's descriptor, charge_type, overlimit and declined strings follow the bank's content pack, so a german or mandarin run needs them overridden.
  • config/agent/GPT-5.5-computer-use.yaml: model_name changes from gpt-5-5-genai-responses to gpt-5-genai-responses.

Test plan

  • New suites: test_buy_with_card.py (rewards across all three outcomes, normalization, composites), test_card_payments.py (authorization), test_vllm_model_args.py (hostname check).
  • Review follow-ups:
    • Declined row: posted only on insufficient credit, never on detail typos. A typo followed by the refusal still scores. Filling the cart without checking out fails, alone and in the composite with the right todo. A second refusal fails.
    • Expiry: parsing, a card is good through its expiry month, an expired card declines, and the shop rejects past or malformed expiries before calling the bank.
  • Reviewer's repro on content=webshop (copier ×5, real card): the declined checkout adds the DECLINED - OPENAPPS SHOP $21,499.95 row. With the shortfall todo, "cart filled, never checked out" scores False and "declined checkout" scores True.
  • Extended: test_openbanking.py, test_onlineshop.py, test_no_egress.py.
  • Full suite on the merged branch: 1743 passed, 8 skipped, 2 failed. Both failures were in TestGemmaCoordsConfig, where main's new test builds a vLLM agent with no hostname and now trips this PR's hostname check. The test now passes a placeholder hostname; that file and test_vllm_model_args.py pass on rerun.
  • Full suite after the review follow-ups: 1784 passed, 8 skipped, 0 failed.
  • End-to-end: buy_the_console_table_with_the_business_card with agent=GPT-5.5-computer-use, record_video=True and W&B runs through and is scored. GPT-5.5 did not complete the purchase on that run (scored fail).

`git worktree add .claude/worktrees/<topic>` puts a full checkout inside the
repo. It showed up as untracked in the main checkout, so a stray `git add -A`
would have committed the entire nested tree.
The shop was a port of Princeton's WebShop that needed OpenJDK 21 and a
~1000-product dataset downloaded from Google Drive by `setup.sh`. Products
were searched through a Lucene index built by pyserini, which is a JNI
binding and hence the JDK. None of that reaches an offline eval node, so the
app shipped disabled and could not be launched from a clean checkout.

Three things made the rewrite cheap. `get_reward`/`get_goals` -- the whole
WebShop scoring model, including its spaCy POS-tagging and thefuzz attribute
matching -- were imported but never called. No task in config/tasks targets
the shop. So nothing depended on the dataset's annotations or on the state
shape, and the schema was free to change.

* Catalog seeds from `config/apps/onlineshop/content/` like every other app
  (40 products, 8 categories), so `content` is a real variation axis.
* Search is SQLite FTS5 with its built-in `bm25()`, weighted title > options
  > bullets > description. Option values are indexed, matching the document
  the original built for Lucene. No JDK, no new dependency.
* Persistence is one SQLite file of ordinary relational tables. Order lines
  were keyed by a stringified Python tuple ("('B07YPYJ32Z', '{}')") read back
  with ast.literal_eval; `/onlineshop_all` now returns plain nested JSON.
* Rendering is FastHTML on the shared design tokens, so `apps/theme=` applies,
  plus a new `layout` group: default, grid, compact_table.
* Product imagery is a generated SVG swatch keyed on the sku. The old cards
  pointed at Amazon CDN URLs, which is egress the eval nodes do not have.

Enabled by default now that it has no setup step. Drops pyserini, spacy,
thefuzz, gdown and faiss-cpu (the last two already unused) -- 33 packages
including pyjnius. setup.sh keeps only the Java 21 install, moved to the map
app which is the actual OpenTripPlanner consumer.

Test plan: uv run -m pytest tests/ -> 772 passed, 6 skipped (was 724/7; the
onlineshop test no longer skips on missing Java). tests/test_onlineshop.py
adds 47 covering search ranking and FTS operator injection, cart merge/split
on options, checkout, the reward state shape, all three layouts, per-app and
global themes, german content, and absence of external hosts.

Not verified: reference screenshots under tests/generated_screenshots for the
onlineshop_* routes still show the old Bootstrap UI and need regenerating;
Chromium would not start in this sandbox.
Three things found while using the shop:

* Cart lines showed a unit price and a quantity but no line total. Added
  "$89.00 each x 3 = $267.00".
* Two lines of the same product differ *only* by their options, which were
  rendered as a small muted caption -- so a cart holding a fine-nib and a
  broad-nib pen read as duplicated rather than as two variants. Options are
  now chips, and a deselected line says so. (The merge itself was correct:
  identical options merge in every path -- urlencoded, multipart, in-process.)
* No way back to the landing page from the cart or an item. Added a "Shop"
  link to the shop bar, plus a cart count. It lives there rather than on the
  header logo because `clickable_logo` defaults to false and is a start-page
  variation axis; navigation must not depend on a difficulty knob.

Also name the tables explicitly. fastlite derives them from the class name and
singularises, so `Order` became `order` -- a SQL reserved word, breaking
`SELECT * FROM order` for anything reading the database directly. That defeats
the point of the schema, which is to be readable by other apps and by humans.
Now products / cart_items / orders / order_items, as documented.

Test plan: uv run -m pytest tests/test_onlineshop.py -> 51 passed (4 new,
covering line totals, option chips, per-page navigation, and table naming).
Verified against a live launch with sqlite3: unquoted joins across all four
tables now work.
The installation page still described the old shop as an optional extra
requiring OpenJDK 21, a gdown dataset and setup_pyserini.sh. Replace that with
what is actually true -- the shop needs no setup and is on by default -- and
add the parts a user needs after launching:

* how to enable/disable it and vary layout, content and theme;
* how to find the SQLite database for the run they were just clicking through
  (`ls -t log_outputs/*/databases/onlineshop.db | head -1`) and query it;
* a short architecture note: catalog seeded from Hydra content, FTS5 + bm25()
  for search, four relational tables, FastHTML on the shared design tokens,
  generated SVG imagery, `/onlineshop_all` as the reward surface.

Kept a collapsed note on the old JDK/Lucene setup, since the Google Drive
links it used are now dead and anyone following an older guide will get stuck.
Added a matching section to the README.

Verified every documented override composes (layout x3, content x3, theme
global and per-app, enable, products_per_page) and the sqlite3 snippets run
against a live launch.
Thumbnails were a coloured square with the product's initials, which made a
listing scan like a spreadsheet. Replace with 35 line-art glyphs -- headphones,
skillet, tent, fountain pen -- picked by keyword from the title with a
per-category fallback, so a product added to the config without a matching
keyword still gets something appropriate rather than a blank.

Still generated rather than fetched, for the same reason as before: the
original hotlinked Amazon CDN URLs, which eval nodes cannot reach and
tests/test_no_egress.py forbids. Deterministic, no dependency, ~2kB each, and
hue stays keyed on the sku so a product looks the same on every page.

Keyword order is load-bearing and was wrong on first pass: "LED Desk Lamp"
matched the standing-desk entry because it contains "desk". Ordering is now
commented and pinned by tests, along with "Desk Organizer" and the
"Backpacking Tent" / "backpack" collision.

Test plan: uv run -m pytest tests/test_onlineshop.py -> 62 passed (11 new).
Audited all 40 products: 34 distinct glyphs, zero falling through to the
category default.
Replace the generated product catalog with an optional importer for the
WebShop item dump, so no model-written product copy ships in the repo.

`scripts/fetch_webshop.py` downloads the dump from its HuggingFace mirror
and converts it into a `webshop` content pack. The output is gitignored:
the records are scraped Amazon listings, so they stay on the machine that
downloaded them rather than being redistributed from here. Field names are
read from candidate lists because the dump has been re-exported several
times -- the current one uses name/full_description/small_description/
pricing -- and `--inspect` prints the real schema when they drift again.
Image URLs are dropped on import, including the ones hidden in
`customization_options` sibling keys, and the script aborts if any survive.

`content/default.yaml` is now chrome only. With an empty catalog the start
page leaves the shop unregistered and drops its tile, so the app is absent
rather than an empty storefront. `content/fixture.yaml` carries the small
mechanical catalog the tests need.

Also fixes two things real titles exposed:

* Glyph keywords matched as bare substrings, so "pen" fired on "Open Toe
  Sandal" and "tent" on "content". They now match whole words with an
  optional plural, and the table is widened against real titles: keyword
  coverage goes from 41/200 to 113/200, the rest taking a category default.
* WebShop's coarse `category` field put nearly everything in one slug, so
  the mapping reads the `product_category` breadcrumb deepest-crumb-first.

`scripts/render_glyph_sheet.py` renders a catalog's line art to one page,
tinting the products that fell back, so coverage can be eyeballed without
launching the app.

Test plan: 799 passed, 6 skipped. Verified against the real dump -- 47
pages over 200 scraped products reference no external host and the seeded
database holds no URLs. ruff clean on both scripts; mkdocs build --strict
clean (site/ left unbuilt).
Keep the source catalog's image URLs and render them when asked, instead of
discarding them at import.

`apps.onlineshop.product_images` picks between `glyphs` (default) and
`hotlink`. Under `hotlink` a product with several images becomes a CSS-only
carousel -- one hidden radio per slide, one clickable dot per image -- so the
controls are real elements an agent can click with no JavaScript and no new
dependency. A single image renders without controls, and a product with none
falls back to its glyph.

The glyph also ships inside every carousel as the fallback: each <img> has an
onerror that reveals it, so a failed fetch degrades to line art rather than a
broken-image icon. One failure takes the whole carousel down deliberately --
the common case is "no network", where all of them fail, and a half-populated
carousel is a worse observation than a consistent one.

Default is `glyphs`, and an unrecognised value falls back to it, because
hotlinking is only safe when the network is really there:

* the eval nodes have no outbound network, so the images do not arrive while
  the page still returns 200 and a screenshot-scored agent gets graded on an
  observation that quietly lost its imagery;
* TestNoEgress fails under `hotlink`, by design;
* for the WebShop catalog the URLs point at Amazon's CDN.

The importer's URL guard is now field-scoped rather than whole-record: text
fields still must not contain a URL, since those render under either mode,
while `images` is the one place a URL is legitimate. Amazon's transparent
spacer gif is filtered out of the image list, and images are capped at four
to match the carousel dot rules.

Test plan: 809 passed, 6 skipped. Verified both modes over the real 200-
product catalog -- `glyphs` emits no product-media markup and references no
external host, `hotlink` emits 34 slides against m.media-amazon.com. Browser
rendering unverified: chromium crashes in this sandbox, so the carousel is
checked at the markup level (checked-radio adjacency, unique radio group per
sku, glyph sibling present) rather than visually.
Six app modules imported their siblings through `src.open_apps...`, which
only resolves because `open_apps/__init__.py` pushes the repo root onto
sys.path on package import. That shim works, but it means the same module is
loaded twice under two names -- `src.open_apps.theme` and `open_apps.theme`
are distinct objects with separate globals -- so identity checks and any
module-level state silently diverge depending on which spelling a caller
used. Import them the way the package is installed instead.

The shim stays: it is a public compatibility path and something outside this
repo may still use the old spelling.

Also guard `render_theme_css` against a theme token containing `<` or `>`,
which could otherwise close the <style> block it is rendered into. No shipped
theme has such a value, so this is defensive only -- the neighbouring checks
already cover the property name and the @import URL.

Test plan: 809 passed, 6 skipped. Confirmed no module remains loaded under
the `src.` name after importing the apps, and that the theme guard drops an
injected `</style><script>` value while leaving quoted font stacks intact.
The shop needed `scripts/fetch_webshop.py` to run before it would appear at
all, which made a fresh clone a two-step setup and left the storefront absent
rather than broken if you skipped it. Check the generated pack in instead:
`apps/onlineshop/content=webshop` is now the whole switch.

- un-gitignore config/apps/onlineshop/content/webshop.yaml (200 products)
- the pack sets `product_images: hotlink`; the shop-wide default stays
  `glyphs`, so only runs that select this catalog reach the CDN
- add an explicit `_self_` to apps/onlineshop/default.yaml so a content pack
  can override an app-level key at all -- without it Hydra composes the app
  defaults last and the pack's `product_images` was silently discarded
- fetch_webshop.py becomes a regenerator rather than required setup, and its
  header round-trips what is committed
- attribution in README/docs now says the data is redistributed here, because
  it is
999 products, up from 200 -- the whole of items_shuffle_1000.json less one
record with an empty title, which `convert` skips. Search, pagination and the
category strip all have something to work with now.

`--limit` defaults to 1000 to match, so a bare `fetch_webshop.py` rebuilds the
committed pack rather than silently shrinking it to 200. Product counts and
the glyph-sheet sample output in the docs are regenerated against the real
catalog (531 titles match a glyph keyword, 468 fall back to the category).
Alignment. The search field had no `flex`, so it kept its intrinsic ~150px
while its form grew to 1020px -- the slack opened as dead space between
"Search" and "Cart" rather than widening the field. It now grows (capped at
34rem) and cart/orders anchor right. The cart's three action forms sat in an
unclassed div and stacked as ragged blocks; `.cart-actions` makes them one
end-aligned group on a shared 7rem button width. The buy row's number input
rendered 9px shorter than its button, so both control rows align on `stretch`
instead of `center`. The footer link moves to its own row, where it no longer
shares a line with a page's closing CTA at a different margin-top.

Measured in chromium at 1280/900/600px: every control in the bar is 30.7px
tall, the three cart buttons share an exact left and right edge at all three
widths, and the buy row's input and button now match at 30.7px.

Java. The shop's search dropped pyserini/Lucene a while back, but the JDK
story was still told in nine places, and `appserver.registered_apps` claimed
`onlineshop` needs Java 21 installed, which is simply false. Removed from the
shop's docs, config and docstrings. The map app's OpenTripPlanner JDK is a
real dependency and is untouched.
`uv run launch.py apps.onlineshop.enable=True` did not produce a shop: the
flag was already the default, and what was actually missing was a catalog.
The default `content` pack was `default`, which is chrome with `products: []`,
so the start page left the routes unregistered. Committing the catalog was
supposed to remove that setup step; leaving it unselected kept it.

`content: webshop` is now the default, so a bare launch has a 999-product
storefront. `content=default` remains the chrome-only pack as an explicit
choice, and the gate still hides the shop when a pack has nothing to sell.

This makes the default configuration reach the network, since the webshop
pack carries `product_images: hotlink`. That was invisible to the egress
suite -- its ROUTES list has no `/onlineshop` -- so `/onlineshop` joins the
ratchet with `m.media-amazon.com` as its one allowed host, the only
content-driven entry among styling dependencies. Eval runs on a node without
egress want `apps.onlineshop.product_images=glyphs`.

Three tests asserted absent-by-default and now assert the inverse; the
empty-pack path they covered is retained against an explicit `content=default`.
…grid

The shop was opt-in on the openbanking branch and is enabled by default on
the shop branch, so the gate has no remaining caller after the rebase.
Cards gain `card_cvv`, masked on the card face and revealed by the same click
that reveals the PAN -- a checkout elsewhere needs it, so it has to be
reachable, but it should cost the agent the extra click.

`authorize_card_purchase` is the app's first write path. It matches every card
detail, approves inside `available_credit + overlimit_grace` ($10 by default),
debits the card and posts the charge, plus a second row naming the overage when
the charge used the grace band. Rows post pending so no wall-clock value
reaches /openbanking_all; declines say nothing about which field was wrong, or
the CVV is three digits of brute force away.

set_environment now clears before re-seeding, like the shop's does -- a reset
has to put the card's figures back rather than collide on the seeded keys.
Checkout's card step was cosmetic: a brand dropdown checked against a config
allow-list, with no number, no expiry and nothing behind it. It now asks for
the PAN, expiry, CVV and cardholder name and authorizes them against a card
held by OpenBanking, which is where the available credit lives.

The apps already share a process -- start_page imports each and merges the
route tables -- so the shop calls the bank directly rather than going back out
over HTTP to its own server. The import is lazy and guarded: a run without the
bank declines cleanly instead of raising out of a checkout handler.

The charge happens before the order is written, because an order with no
matching authorization is the one state a checkout must not leave behind. A
decline redirects back with the issuer's message and leaves the cart intact.
Orders record the paying card's last four, and /onlineshop_all reports it.

Defaults to on: `allowed_credit_cards` now lists the brand the seeded card
actually carries, so the check is exercised rather than skipped.
Checkout has been able to charge a card since 020778d, but nothing measured
it: every task in config/tasks/openbanking.yaml reads a figure off the ledger
and writes the answer into todo/calendar/messenger, because the bank was
read-only. BuyWithCardTask is the other shape -- the scorable delta lands in
the shop and the bank at once, which is the first time either app's state has
had to move together.

What it measures is whether an agent can carry a value across apps. The PAN,
expiry and CVV are only on the card's page in OpenBanking, each behind the
click that unmasks them, and the checkout form takes no order without all
three. The target is a new order carrying the paying card's last four plus a
pending Card row that debits the headroom by exactly the order total, so
buying the right thing on the wrong card still fails.

A checkout writes three things no target can predict: a random order id, a
wall-clock date, and a ledger description embedding that order id. Dropping
the description would throw away the only thing tying a charge to the order
that caused it, so instead every order id in the shop's slice is substituted
with a fixed token on both sides -- the diff still asserts the charge names an
order that exists. Row id and position go too; both are functions of the
bank's insertion scheme rather than of what the purchase did.

The normalization is opt-in, like reply_contacts. Every other task should keep
failing on a spurious order, ids and all, and CompositeTask turns it on itself
when a sub-task buys. A purchase needing the overlimit grace refuses to build
a target rather than score half a ledger it did not describe.

Two tasks ship with it: the purchase alone, and a composite that chains it
with a todo naming the headroom left afterwards -- a figure that does not
exist until the charge posts, so the agent has to cross back.

The tests score against states taken from the running apps rather than a
fixture, since the failure worth catching is the target drifting from what a
checkout actually writes. That is also why the two shipped tasks sit out
test_openbanking's generic self-consistency sweep: states/initial_state.json
predates both apps, and refreshing it would move the ground under every other
task in the repo. The same assertion runs in test_buy_with_card.py instead.
1. update GPT 5.5 model
1. update banking tasks
1. add buy_the_mirrors_just_over_the_card_limit
1. add attempt_the_copier_far_over_the_card_limit
1. record purchase declines
shop-with-bank already carries rebased copies of every webshop-rewrite
commit on top of the theme + layout refactor, so each conflict resolves to
this branch's side. What the merge adds is main's dev server (dev.py,
scripts/dev.sh) and its README notes (dev server, reproducing the paper).
docs/index.md is kept as-is: the auto-merge re-added the pre-refactor
Layout section a third time.
- ModelArgs.make_model raises a ValueError naming the fix when hostname is
  null, instead of building http://None:8000/v1 and failing three minutes
  later as a bare RetryError
- AgentLauncher records errored / err_msg / stack_trace in the W&B summary,
  so a crashed episode no longer looks like a zero-reward run
Serves a self-hosted agent model with --host 0.0.0.0, caches weights under
/checkpoint, clears inherited proxy vars, and writes the node name to
~/logs/vllm_host_<jobid>.txt. Account/QOS/partition are placeholders.
@aaronsmulktis aaronsmulktis self-assigned this Oct 13, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 13, 2026
@aaronsmulktis
aaronsmulktis marked this pull request as ready for review October 13, 2026 13:38
@aaronsmulktis aaronsmulktis changed the title Shop w/Bank [Feature] Pay for shop orders with OpenBanking card; cross-app purchase tasks Oct 14, 2026
main's TestGemmaCoordsConfig builds a self-hosted agent without a hostname, which this branch's fail-fast now refuses. The tests build the client but never call it, so any name will do.
@aaronsmulktis aaronsmulktis changed the title [Feature] Pay for shop orders with OpenBanking card; cross-app purchase tasks [Feature] Pay for shop orders with an OpenBanking card; cross-app purchase tasks Oct 14, 2026
…xpiries

A refusal for insufficient credit now posts one zero-amount DECLINED row on
the card, so a refused checkout is distinguishable from a cart that was
filled and never submitted. DeclinedCardPurchaseTask scores that row
alongside the cart line. Detail typos still write nothing.

Expiry is parsed as a real month/year and judged against the bank's pinned
as_of_date; checkout refuses a past or malformed expiry before asking the
bank, and an expired card on file declines.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant