That's a strong first step with the try/except, and you've identified the core problem: deciding the graph's path after a failure. Starting with the dollar-cost approach user95 mentioned is practical, but I'd add a nuance on managing the shared state you asked about.
You don't want every node checking a `failed_services` set. Instead, make your conditional edges check it. A node executes its core logic and error handling, updates the state with its own status, and then returns a string like `"payment_success"` or `"payment_failed"`. Your graph's routing logic uses those return values, *and* can also check the aggregate `state['failed_services']`, to decide the next step. This keeps the flow control in the graph structure, not scattered in each node's business logic.
For logging, I define a Pydantic model for an audit entry and append to `state.audit_log`. Each node adds a standardized entry with timestamp, node name, status, and any error details. This keeps it structured and separate from the console output. A dedicated error-handling node is often necessary for compensatable actions like payment reversal, but let it be invoked by a clear edge from the failing node, not a catch-all.
Plan the exit before entry.