Independent authorization and evidence for AI agents executing high-consequence transactions.
SecondSign enforces the boundary before execution, then records exactly what was
authorized. Wallets are one rail — not the product boundary.
One screen, three vantage points: the agent that proposes, the human who decides, and the money that only moves when both sides of the boundary agree.
┌──────────────────────────────┬──────────────────────────────┐
│ ① the agent's terminal │ ② the approver's browser │
│ │ │
│ $42 payment │ SecondSign · open reviews │
│ → completed ✓ │ ┌─────────────────────────┐ │
│ │ │ $300.00 · held for review│ │
│ $300 payment │ │ to fp:abab… │ │
│ → awaiting_review ⏸ │ │ [Approve] [Decline] │ │
│ (parked for a human) │ └─────────────────────────┘ │
│ → completed ✓ on approval │ │
│ │ │
│ $900 payment │ │
│ → refused ✗ (over the cap) │ │
├──────────────────────────────┴──────────────────────────────┤
│ ③ the rail's own ledger, live │
│ 14:02:11 request #1 arrived via=gateway │
│ 14:02:39 request #2 arrived via=gateway (and no #3) │
└─────────────────────────────────────────────────────────────┘
The agent container in ① holds no payment credential and no network route to the rail — its only way to money is a proposal to the gateway. The panel in ② talks to the gateway over a second mTLS channel with its own CA and its own network, which the agent cannot reach. The ledger in ③ is read at the destination, because "my attempt failed" and "nothing arrived" are different statements.
pip install secondsign-core # the engine (Python 3.11+)
pip install "secondsign-core[stripe]" # plus the Stripe rail driverThe whole decision path in one script — three proposals through the real
gateway authorize() / resolve(), on a mock rail that moves no money:
pip install secondsign-core
python examples/quickstart.py # one script, imports only the public API agent proposes $42 → completed ✓ money moved (mock)
agent proposes $300 → awaiting_review ⏸ parked for a human
approver clicks Approve → executed ✓ money moved (mock)
agent proposes $900 → refused ✗ value_band_exceeded
examples/quickstart.py is self-contained on the
public API — clone the repo to run it, or copy it anywhere. For the
production-faithful topology — two networks, mTLS, the agent in its own
container with no rail code — run the demo below.
git clone https://github.com/Bestpart-Irene/secondsign-core
cd secondsign-core/deploy/reference
python tls/generate.py # ephemeral two-CA PKI, never committed
docker compose -f compose.yaml -f compose.demo.yaml up --build -d
# ② open http://127.0.0.1:8090 — the approver panel
python demo/run_demo.py # ① three proposals: $42 / $300 / $900
python demo/watch.py # ③ the rail's ledger, live
docker compose -f compose.yaml -f compose.demo.yaml down -vThe $300 proposal will sit at awaiting_review until you press Approve
in the panel — then the agent's own re-send of the same handle reads
completed, and one request appears on the ledger. Decline it instead and
nothing moves. Details and the security properties of the topology:
deploy/reference/.
Give an AI agent an execution tool and you have given it the ability to create an irreversible consequence. A bad sentence can be retracted with an apology; a wrong wire, trade or token transfer cannot.
The usual answers are a better prompt, an eval suite, and a safety function the agent is told to call first. All three share one flaw: the agent decides whether to obey them. Anything an agent can skip is not a control.
SecondSign takes that decision away from the agent.
Once an agent acts for a customer, a company or a capital owner, solving that first problem exposes a second one: the party carrying the risk will eventually ask the operator to prove the agent stayed inside what was authorized. Logs written by the same system that made the mistake do not answer that question; they are the operator grading its own homework. SecondSign makes enforcement primary and evidence its necessary by-product: an out-of-mandate action does not cross the execution boundary, and every decision binds the exact request that was authorized.
An independent authorization and evidence layer on the execution path. The agent can propose an action. Only the boundary can authorize and dispatch it.
The enforcement mechanism follows the rail: an execution gateway for API payments and brokerage, a transaction co-signer for a smart account, and a closed adapter for each new action domain. The invariant does not change: the agent must not possess another route to the irreversible effect.
That separates category from wedge. The category is independent execution authorization and evidence. The current commercial product is an independently operated boundary for agents handling third-party capital. Agent wallets are the first product wedge.
The agent holds no bank, broker or processor credential, and has no network route to them. Its only route to the money is a request to SecondSign, and SecondSign answers it the same way every time.
The test that falsifies a deployment: turn SecondSign off. If the agent can still move money, you have not installed a boundary — you have installed a library it is free to skip.
There is now a deployment that passes that test, and running core in-process
still does not. CORE-S019 builds the shape — a
standalone gateway process holding the credentials, the agent on the other side
of a process boundary with a client distribution that contains no rail code at
all — and deploy/reference/ is a two-network topology you
can copy. CI stands it up, runs an adversarial suite inside the agent
container written against the standard library rather than against the client,
and then re-runs that suite against a deliberately joined topology and requires
it to fail there, because a gate that cannot be made to fail is not evidence.
Installed the other way — the library imported into your agent's process — it is still a control your own code chooses to route through, and the falsification test still fails. That is right for development and evaluation, and it is not production custody of money. Status says what is left.
financial agent
│ "pay invoice 4471, $2,500, to a new supplier"
▼
IntentAdapter trust boundary — raw account and customer data stop here
│
▼
TransactionIntent immutable; fingerprints and whole cents, never a card number
│
▼
Policy → Decision ALLOW / REVIEW / DENY — combining can only tighten
│ └── REVIEW → MakerChecker: a human, one shot, expiring
▼
ExecutionGateway re-checks the request is still the approved one, then sends it once
│
▼
AuditReceipt redacted, hash-chained — a later edit is detectable
In plain terms:
- Adapter. The agent's tool call becomes a structured, immutable request. Account numbers and customer records cannot cross this line; amounts are whole cents, never floats.
- Decision. Your rules return allow, hold for review, or deny. Run ten rules and they can only make the answer stricter — no rule can overrule another one's "no", and no rule can grant permission.
- Human approval, when it is warranted. The approval is one-shot, expires, and is bound to that exact request. Approve a $2,500 invoice and nothing else can ride on that approval.
- Execution. Right before sending, the gateway re-checks that the request is byte-for-byte the one that was approved, then sends it exactly once — with an idempotency key SecondSign derives, never one the agent supplies.
- Receipt. What was decided, who approved it, what happened. Redacted, and chained by hash so tampering shows.
pip install secondsign-core # the engine
pip install "secondsign-core[stripe]" # plus the Stripe railfrom datetime import datetime, timedelta, timezone
from secondsign.adapters import StripeAdapter, StripeCall
from secondsign.contracts import Currency, SourceTrust
from secondsign.decision import DecisionEngine
from secondsign.intent import PaymentTargetKind, SettlementPriority
from secondsign.policy import (
AggregateKey,
AmountLimit,
AmountWindowPolicy,
PolicyContext,
WindowAggregate,
)
now = datetime.now(timezone.utc)
# 1. The agent asks to pay. The adapter turns the tool call into an immutable
# request — account identifiers never enter, only fingerprints of them.
call = StripeCall(
counterparty_ref="fp:" + "a1" * 32,
source_account_ref="fp:" + "b2" * 32,
not_before=now,
not_after=now + timedelta(minutes=5),
declared_source_trust=SourceTrust.trusted_instruction,
scope_count=1,
amount_minor=250_000, # $2,500.00 — always integer minor units
quote_currency=Currency.USD,
target_kind=PaymentTargetKind.bank_account,
new_beneficiary=True,
cross_border=False,
settlement_priority=SettlementPriority.standard,
)
intent = StripeAdapter().derive(call)
# 2. Your rule: at most $1,000 an hour to this counterparty.
policy = AmountWindowPolicy(
AmountLimit(quote_currency=Currency.USD, window_seconds=3600, max_aggregate_minor=100_000)
)
context = PolicyContext(
window_aggregate=WindowAggregate(
key=AggregateKey.from_intent(intent),
window_seconds=3600,
aggregate_minor=0, # nothing spent in this window yet
count=0,
)
)
# 3. The decision.
decision = DecisionEngine([policy]).decide(intent, context)
print(decision.verdict.name, [reason.value for reason in decision.reasons])
# DENY ['value_band_exceeded']That is the decision primitive in isolation. The full path — proposed, held,
approved by a second human, executed, and receipted — runs in
examples/quickstart.py (no Docker), and is proven
against real test-mode Stripe in
tests/e2e/test_vertical_path.py.
Wallets are the sharpest current product wedge, not SecondSign's category. The same authorization engine governs this execution domain: an agent whose rail is a blockchain account. The account is a Safe smart account owned 2-of-2 — the agent's key and SecondSign's co-signer — so the co-signer's signature is the ALLOW verdict. A refused proposal is simply never signed, and a transaction with one signature of two cannot execute. There is no separate enforcement step for the agent to skip.
Before it signs anything, the co-signer re-reads the Safe's live state —
owners, threshold, guard, nonce — and the token's on-chain identity, and
refuses on any drift from what was configured. The signing key sits behind a
provider contract, never in the co-signer itself. And on the chain, a pair of
Solidity guards (a transaction guard and a module guard, Safe 1.5.0, under
onchain/) refuse by revert any transaction that would change the
account's control — replace the guard, change owners or threshold, enable a
module, delegatecall — on both Safe execution paths. The guards judge
integrity only, never value: amounts and counterparties are the co-signer's
decision, made off-chain by the same deterministic engine as everything above.
SecondSign is not a wallet and takes no custody — it holds one key of two, and the account is yours: no funds move onto anyone's platform in order to be protected. It issues no token, and no authorization may ever depend on holding one.
Watch the co-signer at work against a real Safe on a local chain (needs Foundry):
python examples/onchain_firewall_demo.py --out /tmp/ss-demoFour proposals: a small transfer is co-signed and the USDC moves; a large one
is held for a human and executes on approval; an unlimited approve to an
unvouched spender gets no signature; and the agent's own attempt to remove
SecondSign (setGuard(0)) gets no signature either. Every verdict and
signature comes from the actual co-signer — only the ERC-20 is a local
stand-in.
This domain is younger than the fiat path and says so plainly: the contracts are unaudited, nothing has run beyond a local chain, and Status names the rest. What it defends against, and why each rule exists: On-chain threat model.
Each of these is a promise bound to the test that enforces it. See Invariants.
- Fail closed. Anything unclear, missing or unavailable takes the strictest path. Silence is never consent.
- Only ever stricter. More rules, plugins or enterprise extensions can tighten a decision. Nothing can loosen one.
- What was decided is what gets executed. Bound by a digest, re-verified in the instant before dispatch.
- Approvals are single-use. Tied to one request, with an expiry.
- Credentials never leave the gateway. They cannot appear in a request, a receipt, a plugin's input, or an error message.
- No raw financial or customer data in decisions, receipts or logs.
- Deterministic. No model sits on the live decision path. The same request gets the same answer, and you can explain that answer to an auditor.
SecondSign fits an agent deployment when three things are true: the action has a costly or irreversible consequence, every route to that consequence can be placed behind an execution boundary, and another party needs evidence of what was authorized.
The sharpest current fit remains a team whose agents control money that belongs to someone else — where the capital's owner can ask, at any moment: prove the agent stayed inside what I authorized.
- An operator running treasury, trading or DeFi agents over a client's or a fund's capital.
- An agentic-commerce or payments team whose agents touch user or merchant balances.
- A company selling agents into enterprises, where the deal stalls on how would we ever let this near production money? — "every financial action passes an independent co-signer the agent cannot bypass" is the answer that unblocks it.
- A fintech or vertical SaaS shipping agent features that need a control an auditor will accept.
The same boundary also protects a team spending its own budget from a prompt-injected or simply wrong agent — that is where many deployments start. As closed adapters and execution boundaries are added, the same model can govern other high-consequence transaction rails. It is not a wallet, a model-safety layer, a prompt filter, an agent framework, or a passive monitoring tool. It has one job, at one moment: the instant before an irreversible action is executed.
SecondSign is not an insurance company and does not absorb an agent deployment's losses onto its own balance sheet. It supplies structural prevention and a hash-chained decision record: controls that can make a deployment's risk more bounded, explainable and potentially insurable.
The operator still owns the agent, its integration and the consequences of actions allowed by its policy. The policy authority owns the boundary it sets; a named human owns an approval they give. SecondSign's responsibility is the narrower question of whether the execution boundary it supplies worked as specified. The resulting record narrows factual disputes about the request, verdict, approval and dispatch; contracts, applicable law and any actual insurance policy determine the final allocation of loss.
| SecondSign Core — this repository, Apache-2.0 | The decision path: contracts, intent, policy, decision, human approval, gateway, local audit, rail adapters, and the conformance kits third parties test against. Useful on its own, and it always will be — this is not a crippled edition. |
| SecondSign Enterprise — separate, commercial | Organisational scale: hosted runtime and control plane, multi-tenancy, org-wide policy, centralised audit, remote approvals, SSO/RBAC, compliance workflows, and attestation that a deployment really is what it claims. |
Two rules hold that line: core never depends on anything private, and an enterprise extension may only make a decision stricter — never grant a permission core would have refused.
The split follows the trust model, not a feature ledger. The open core is the whole mechanism, and operated by your own team it is a strong internal control — but an authorization boundary you run yourself is still your own word, which your clients, auditors and insurers must take on faith. Independence — the boundary operated by a party the agent's operator does not control, under a policy the risk owner is party to and that can only ever be tightened — is a property of who runs a deployment, never of code. That independently operated form is what the commercial layer exists to be: independence cannot be self-hosted.
Extensions — a new rail, a policy plugin, an approval provider — prove they are safe by inheriting a conformance test suite, not by persuading a maintainer. See Extension contracts.
Pre-1.0. Interfaces may still change.
Built and tested: the whole decision path end to end — contracts and the
plugin boundary, intent, policy, the decision engine, maker-checker approval,
the execution gateway, the hash-chained audit receipt, Stripe and Alpaca
adapters, the conformance kits, and an adversarial matrix run against the threat
model. Around it, the deployment shape: the standalone gateway process, the
credential-free client distribution, the reference two-network topology, and a
held REVIEW that reaches a human on a second mTLS channel the agent has no
route to — and comes back as an executed payment when, and only when, that
human approves. On the wallet side: the Safe co-signer path with live-chain
re-verification before every signature, the signing key behind a provider
contract, the constitutional double guard in Solidity, and a timelocked,
account-vetoable recovery path for a lost co-signer key. Branch coverage is
100%, enforced by CI rather than asserted here — but read that as an
engineering signal, not as evidence of security. It says every branch was
executed by some test. It does not say the tests assert the right things, and
it is not a substitute for the independent review this project has not yet had.
Not there yet: named rather than rounded off. The control-plane state the gateway keeps (the principal fingerprint key, the spend window, pending reviews) lives in the process, so a restart forgets it. Spending limits are a constant in the gateway rather than state under an auditable authority. And the on-chain path is younger than the fiat one: the contracts have had no independent audit, nothing has run beyond a local chain, and the decided effect is still read from calldata rather than from simulation. Running the library inside your agent's process remains right for development and testing, not for production custody of money.
Where each queued slice actually stands, derived from Git rather than
hand-maintained: docs/slices/STATUS.md.
| Architecture | What core is, and what it deliberately is not |
| Threat model | What this defends against, and why each rule exists |
| On-chain threat model | What changes when the agent holds a wallet, and what the guards must hold |
| Invariants | The guarantees, each bound to the test that enforces it |
| Extension contracts | How to add a rail, rule or provider and certify it |
| Contributing | The slice protocol and quality gates |
| Governance | Who decides what, and how little needs deciding |
| Security | How to report a vulnerability, privately |
| Support | Where to start reading, building, and asking |
| Releasing | How a version reaches PyPI |
| Changelog | What changed in each release |
| Roadmap | The build queue, machine-validated |
| Status | Where each slice stands, derived from Git |
Everything needed to build on or contribute to this project is in this repository. Nothing here depends on a private one.
Discord — questions while you are building, and what people are building with it. Issues and Discussions remain the durable record; chat is for the parts that never make it into either.
SecondSign Core is an independent implementation. Its history begins at its own initial commit and shares no Git history with any other project.
Where another project's work informed this one, it is named in
NOTICE with its licence rather than left implicit — including the
architectural patterns adopted from Doberman-Core (Apache-2.0), and the handful
of explanatory comments adapted from it.
Specifications are committed before the implementations they describe, so the commit order is itself part of the record. Every commit carries a DCO sign-off, and any third-party source that informed a change is named in its pull request along with the licence it carries.
Apache-2.0. Copyright 2026 SecondSign contributors. See LICENSE.
The licence text was fetched from https://www.apache.org/licenses/LICENSE-2.0.txt.
Every commit requires a DCO sign-off. See CONTRIBUTING.md.