Skip to content

Commit 22ce3b1

Browse files
authored
Merge pull request #180 from PortalTechnologiesInc/docs/age-verification
docs: add age verification guide and env vars
2 parents a6d9f0a + 866d8dd commit 22ce3b1

6 files changed

Lines changed: 225 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Any language works — Python, Go, Ruby, PHP, Java, TypeScript. No SDK required.
5757
## What you can do
5858

5959
- **Authenticate users** — passwordless login via Nostr identity
60+
- **Age verification** — browser-based identity verification with cryptographic proof ([guide](https://portaltechnologiesinc.github.io/lib/guides/age-verification.html))
6061
- **Request payments** — single, recurring, or invoice-based; BTC (sats) or fiat (EUR, USD, and [more](https://portaltechnologiesinc.github.io/lib/sdk/api-reference.html))
6162
- **Issue JWTs** — signed by the user's Nostr key, verifiable server-side
6263
- **Cashu tokens** — mint, burn, and transfer ecash

crates/portal-rest/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Portal REST API
22

3-
REST API server for Portal — Nostr key auth, Lightning payments, JWT, Cashu, profiles, and more. Use it via the [TypeScript SDK](https://www.npmjs.com/package/portal-sdk) or call the HTTP endpoints directly.
3+
REST API server for Portal — Nostr key auth, Lightning payments, age verification, JWT, Cashu, profiles, and more. Use it via the [TypeScript SDK](https://www.npmjs.com/package/portal-sdk) or call the HTTP endpoints directly.
44

55
**Full documentation:** [https://portaltechnologiesinc.github.io/lib/](https://portaltechnologiesinc.github.io/lib/)
66

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* [Running a Custom Mint](guides/running-a-mint.md)
3636
* [JWT Tokens (Session Management)](guides/jwt-tokens.md)
3737
* [Relay Management](guides/relays.md)
38+
* [Age Verification](guides/age-verification.md)
3839

3940
# Resources
4041

docs/getting-started/environment-variables.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ Publish your service's Nostr profile at startup. All fields are optional — omi
6565
| `profile.picture` | `PORTAL__PROFILE__PICTURE` | No || Avatar URL. |
6666
| `profile.nip05` | `PORTAL__PROFILE__NIP05` | No || NIP-05 verified identifier. |
6767

68+
### Verification
69+
70+
Age verification via Portal's verification service. Required for `POST /verification/sessions`.
71+
72+
| Config key | Env var | Required | Default | Description |
73+
|------------|---------|----------|---------|-------------|
74+
| `verification.api_key` | `PORTAL__VERIFICATION__API_KEY` | No || API key for the Portal verification service. Create one from the [PortalHub dashboard](https://hub.getportal.cc). Required to use the verification endpoints. |
75+
6876
## Minimal setup
6977

7078
```bash

docs/guides/age-verification.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Age Verification
2+
3+
Verify a user's age through Portal's browser-based verification service. The flow uses Cashu tokens as cryptographic proof — tokens have no monetary value, they serve as tamper-proof verification tickets.
4+
5+
## How it works
6+
7+
1. Your backend creates a verification session → gets a `session_url`
8+
2. Redirect the user to the `session_url` in their browser
9+
3. The user completes identity verification
10+
4. Portal mints a Cashu verification token and returns it via the event stream
11+
5. Your backend receives the token — verification complete ✅
12+
13+
The entire flow is handled by a single SDK call (`createVerificationSession`), which creates the session and automatically starts listening for the token.
14+
15+
## Prerequisites
16+
17+
- A **PortalHub** account at [hub.getportal.cc](https://hub.getportal.cc) — create your verification API key and manage your dashboard from there
18+
- `[verification] api_key` configured in portal-rest
19+
- A wallet configured (NWC or Breez) — needed for relay connectivity
20+
21+
## Configuration
22+
23+
1. Sign up / log in at [hub.getportal.cc](https://hub.getportal.cc)
24+
2. Create a verification API key from the dashboard
25+
3. Add it to your `config.toml`:
26+
27+
```toml
28+
[verification]
29+
api_key = "your-api-key"
30+
```
31+
32+
Or via environment variable:
33+
34+
```bash
35+
PORTAL__VERIFICATION__API_KEY=your-api-key
36+
```
37+
38+
## Creating a verification session
39+
40+
<custom-tabs category="sdk">
41+
42+
<div slot="title">HTTP</div>
43+
<section>
44+
45+
```bash
46+
# Create session (relays are optional — defaults to [nostr] config)
47+
curl -s -X POST $BASE_URL/verification/sessions \
48+
-H "Authorization: Bearer $AUTH_TOKEN" \
49+
-H "Content-Type: application/json" \
50+
-d '{}'
51+
# → {
52+
# "session_id": "abc-123",
53+
# "session_url": "https://verify.getportal.cc/?id=abc-123",
54+
# "ephemeral_npub": "npub1...",
55+
# "expires_at": 1234567890,
56+
# "stream_id": "def-456"
57+
# }
58+
59+
# Poll for the verification token
60+
curl -s "$BASE_URL/events/def-456" \
61+
-H "Authorization: Bearer $AUTH_TOKEN"
62+
# → cashu_response event with the token when verification completes
63+
```
64+
65+
</section>
66+
67+
<div slot="title">JavaScript</div>
68+
<section>
69+
70+
```typescript
71+
import { PortalClient } from 'portal-sdk';
72+
73+
const client = new PortalClient({
74+
baseUrl: 'http://localhost:3000',
75+
authToken: 'your-token',
76+
});
77+
78+
// Single call — creates session + listens for token
79+
const session = await client.createVerificationSession();
80+
81+
console.log(`Redirect user to: ${session.session_url}`);
82+
83+
// Wait for the user to complete verification
84+
const result = await client.poll(session, {
85+
intervalMs: 1000,
86+
timeoutMs: 5 * 60 * 1000,
87+
});
88+
89+
if (result.status === 'success') {
90+
console.log('Verified!', result.token);
91+
} else {
92+
console.log('Failed:', result);
93+
}
94+
```
95+
96+
</section>
97+
98+
</custom-tabs>
99+
100+
## Custom relays
101+
102+
By default, the session uses the relays from your `[nostr]` config. Override per-request:
103+
104+
<custom-tabs category="sdk">
105+
106+
<div slot="title">HTTP</div>
107+
<section>
108+
109+
```bash
110+
curl -s -X POST $BASE_URL/verification/sessions \
111+
-H "Authorization: Bearer $AUTH_TOKEN" \
112+
-H "Content-Type: application/json" \
113+
-d '{ "relays": ["wss://relay.damus.io"] }'
114+
```
115+
116+
</section>
117+
118+
<div slot="title">JavaScript</div>
119+
<section>
120+
121+
```typescript
122+
const session = await client.createVerificationSession([
123+
'wss://relay.damus.io',
124+
]);
125+
```
126+
127+
</section>
128+
129+
</custom-tabs>
130+
131+
## Requesting a token from a verified user
132+
133+
If a user already holds a verification token (e.g. verified through the mobile app), you can request it directly:
134+
135+
<custom-tabs category="sdk">
136+
137+
<div slot="title">HTTP</div>
138+
<section>
139+
140+
```bash
141+
curl -s -X POST $BASE_URL/verification/token \
142+
-H "Authorization: Bearer $AUTH_TOKEN" \
143+
-H "Content-Type: application/json" \
144+
-d '{
145+
"recipient_key": "USER_PUBKEY_HEX",
146+
"subkeys": []
147+
}'
148+
# → { "stream_id": "..." }
149+
# Poll events for cashu_response
150+
```
151+
152+
</section>
153+
154+
<div slot="title">JavaScript</div>
155+
<section>
156+
157+
```typescript
158+
const op = await client.requestVerificationToken(userPubkeyHex, []);
159+
const result = await client.poll(op, { intervalMs: 1000, timeoutMs: 60_000 });
160+
```
161+
162+
</section>
163+
164+
</custom-tabs>
165+
166+
## Token lifecycle
167+
168+
- **Web verification** tokens have an amount of 1 (single-use ticket)
169+
- **Mobile app** tokens have an amount of 500 (reusable across services)
170+
- Tokens use Portal's mint (`https://mint.getportal.cc`) with unit `multi`
171+
- Cashu is used purely as a protocol — tokens carry no monetary value
172+
- To prevent replay attacks, **burn the token** after receiving it (see [Cashu Tokens guide](cashu-tokens.md))
173+
174+
## Verification statuses
175+
176+
| Status | Description |
177+
|--------|-------------|
178+
| `success` | Verification passed. `token` field contains the Cashu token. |
179+
| `rejected` | Verification failed. `reason` may contain details. |
180+
| `insufficient_funds` | Mint could not issue the token. |
181+
182+
---
183+
184+
- [Cashu Tokens](cashu-tokens.md) · [Environment Variables](../getting-started/environment-variables.md) · [API Reference](../sdk/api-reference.md)

docs/sdk/api-reference.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,36 @@ See [Cashu guide](../guides/cashu-tokens.md) and [Java SDK](https://github.com/P
170170

171171
</custom-tabs>
172172

173+
## Verification
174+
175+
<custom-tabs category="sdk">
176+
177+
<div slot="title">JavaScript</div>
178+
<section>
179+
180+
| Method | Description |
181+
|--------|-------------|
182+
| `createVerificationSession(relays?): Promise<VerificationSession>` | Create an age verification session. Returns `session_url` to redirect the user to. Optionally pass relay URLs. |
183+
| `requestVerificationToken(recipientKey, subkeys): Promise<StreamOp>` | Request a verification token from a user who already holds one. |
184+
185+
See [Age Verification guide](../guides/age-verification.md).
186+
187+
</section>
188+
189+
<div slot="title">Java</div>
190+
<section>
191+
192+
| Request class | Description |
193+
|---------------|-------------|
194+
| `CreateVerificationSessionRequest(relays?)` | Create an age verification session. |
195+
| `RequestVerificationTokenRequest(recipientKey, subkeys)` | Request a verification token from a user. |
196+
197+
See [Age Verification guide](../guides/age-verification.md).
198+
199+
</section>
200+
201+
</custom-tabs>
202+
173203
## Events
174204

175205
<custom-tabs category="sdk">

0 commit comments

Comments
 (0)