All jobs

Add Stripe Checkout to a Rails App

Yes. An agent adds a Checkout controller and a verified Stripe webhook to your Rails app, tests them with the Stripe CLI and opens a pull request.

Run this on RushIn a Project that has your repository.

What the agent does

The agent reads the app's own conventions before it writes a line. In a Rails app like basecamp/once-campfire it finds .ruby-version at 4.0.7, a Gemfile that tracks rails from the main branch on GitHub, SQLite as the database in config/database.yml, Resque for jobs, Hotwire through turbo-rails and stimulus-rails, and Minitest with Capybara, Mocha and WebMock in the test group. config/routes.rb is resourceful throughout (resource :account, resources :rooms, nested scope module: blocks), and config/ci.rb defines what bin/ci runs: bin/rubocop, bin/bundler-audit, bin/importmap audit, bin/brakeman, bin/rails test, the system tests and a seed replant.

Authentication shapes where the webhook can live. ApplicationController includes the Authentication concern, whose included block adds before_action :require_authentication and protect_from_forgery using: :header_only, with: :exception. Stripe sends neither a session cookie nor a CSRF header, so the agent writes Webhooks::StripeController with allow_unauthenticated_access, the class method the concern already defines, and skips forgery protection on that controller only. It reads the payload with the RawRequestBody concern in app/controllers/concerns/raw_request_body.rb, which the bot message controllers already use to rewind and read request.body, and passes those bytes and the Stripe-Signature header to Stripe::Webhook.construct_event. A bad signature answers 400 before anything is written.

The buying side follows the same style as the rest of the app. The agent adds gem "stripe" with bundle add, a resource :checkout, only: %i[ new create show ] route so create makes the Checkout Session and show is the success page, and a CheckoutsController that requires a signed-in user and puts Current.user.id in client_reference_id. The secret key, price id and webhook secret are read from the environment. For the record it writes a migration and a Purchase model with a unique index on the Checkout Session id, so a retried event is ignored, and db/schema.rb changes with it. The success page is an ERB view in app/views/checkouts/ that reads the Session back from Stripe rather than trusting the query string.

Then it proves it. It runs bin/setup --skip-server, starts bin/rails server, and in a second shell runs stripe listen --forward-to localhost:3000/webhooks/stripe with the test key, sets the printed signing secret, and runs stripe trigger checkout.session.completed. It checks the log line for a 200, checks the Purchase row in SQLite, and sends one request with a forged signature to see the 400. It writes Minitest tests for both controllers that build a correctly signed payload with Stripe::Webhook::Signature.compute_signature, then runs bin/rails test, bin/rubocop and bin/brakeman so the pull request shows the same checks bin/ci would.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Add Stripe Checkout to this Rails app for one fixed-price product. Add the stripe gem, a CheckoutsController (new, create, show) behind the existing Authentication concern that creates a Checkout Session for STRIPE_PRICE_ID with the current user's id as client_reference_id, and a success page that reads the Session back from Stripe. Add a Webhooks::StripeController at POST /webhooks/stripe that uses allow_unauthenticated_access, skips forgery protection on that controller only, reads the raw body with the RawRequestBody concern, verifies the Stripe-Signature header with STRIPE_WEBHOOK_SECRET, and records checkout.session.completed in a new Purchase model with a unique index on the Session id. Test it in the sandbox with stripe listen and stripe trigger checkout.session.completed, and send one request with a bad signature. Add Minitest tests, run bin/rails test, bin/rubocop and bin/brakeman, and open a pull request with the stripe listen, trigger and test output in its description.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

A pull request that adds stripe to the Gemfile and Gemfile.lock, two routes in config/routes.rb, app/controllers/checkouts_controller.rb, app/controllers/webhooks/stripe_controller.rb, a Purchase model and its migration, the updated db/schema.rb, the success view, and two controller tests under test/controllers/. Its description names the environment variables the code reads and quotes what the sandbox printed: the forwarded checkout.session.completed, the 200 in the Rails log, the 400 for the forged signature, and the summaries from bin/rails test, RuboCop and Brakeman.

A reviewer reads the webhook controller first. The forgery skip and allow_unauthenticated_access should apply to that controller alone, never to ApplicationController, and the signature check should run before any model is touched. Then the unique index: it is what turns a replayed event into a no-op. Brakeman's output in the description shows whether the unauthenticated endpoint raised a warning, and the transcript has the full output of every command if a summary is not enough.

Run it on every client app that needs Checkout

When a client signs off on payments, your backend sends one request with that client's repository in resources and the product and price in the prompt, keyed with an Idempotency-Key so a retry returns the same run. Each client's work happens in its own sandbox with its own clone, so two clients never share a working copy or a test database.

Give the API key a webhook URL and Rush POSTs run.completed when the turn ends. The payload carries execution_id and session_url; your server reads the session for the pull request link and attaches it to the client's ticket. Store the execution_id from the 202 against the client when you start the run, since the request body has no field for your own ids.

The use case this job follows, with its automation and trigger: Run an agent per customer task.

Why run it on Rush?

A Stripe integration is only tested when a real signed event reaches a real server. On a laptop that means a Rails server, the Stripe CLI logged in, a forwarded port and a webhook secret in someone's shell. In a Rush sandbox the agent runs all of it on a machine that exists for this one pull request, with the Stripe test key read from a Project secret and nothing left behind on anyone's computer.

The proof travels with the change. The pull request quotes the forwarded event, the log line, the refused forgery and the test run, and the session transcript keeps each command with its full output. A reviewer merging payment code can check the claim against what actually ran instead of taking the description on trust.

Frequently asked questions

Will it weaken CSRF protection for the rest of the app?

It should not, and the prompt says so explicitly. Forgery protection is skipped on Webhooks::StripeController only, where the Stripe signature is the check instead. Every other controller keeps the protect_from_forgery line from the Authentication concern, and the diff makes that easy to confirm.

Does it use Rails credentials or environment variables?

The prompt asks for environment variables, which is what the sandbox provides from a Project secret. If your app keeps keys in config/credentials.yml.enc, say so in the prompt and the agent reads them through Rails.application.credentials instead, leaving the encrypted file for you to update.

My app runs on Postgres, not SQLite. Does that change anything?

Only the setup in the sandbox. The agent reads config/database.yml, starts the database it names, runs the migration against it and tests there. The model, the unique index and the controllers are the same either way.

Can I see the test run before I merge?

Yes. The pull request description quotes the output, and GET /o/{org}/sessions/{id}/messages returns the transcript with every command as a tool_use event and its output as the matching tool_result, linked by call_id.